├── views ├── partials │ ├── tmp.txt │ ├── footer.ejs │ ├── header.ejs │ └── navbar.ejs ├── not_found.ejs ├── users │ ├── users.ejs │ ├── edit_user_password.ejs │ ├── user.ejs │ ├── edit_user.ejs │ ├── add_user.ejs │ └── user_permissions.ejs ├── login.ejs ├── jobs │ ├── jobs.ejs │ ├── add_job.ejs │ ├── job.ejs │ └── edit_job.ejs ├── tickets │ ├── complete_ticket.ejs │ ├── tickets.ejs │ ├── add_ticket.ejs │ ├── edit_ticket.ejs │ └── ticket.ejs ├── clients │ ├── clients.ejs │ ├── add_client.ejs │ ├── client_info.ejs │ └── edit_client.ejs ├── transactions │ ├── transactions.ejs │ ├── transaction.ejs │ ├── add_transaction.ejs │ └── edit_transaction.ejs └── dashboard.ejs ├── public ├── scripts │ ├── barcharts.js │ ├── history.js │ ├── dropdown_search.js │ ├── confirm_password.js │ ├── sidebar.js │ ├── items.js │ └── charts.ejs └── styles │ ├── page_layout.css │ ├── my_account.css │ ├── login.css │ ├── information_card.css │ ├── items.css │ ├── dashboard.css │ ├── styles.css │ ├── navbars.css │ └── the-datepicker.css ├── .gitignore ├── mongod ├── Project Images ├── Clients.png ├── Dashboard.png ├── Add Ticket.png ├── Edit Transaction.png ├── User Permissions.png └── Client Information.png ├── functions ├── isLoggedIn.js ├── isAdministrator.js ├── numberWithCommas.js ├── isManager.js └── isAdministratorOrCurrentUser.js ├── models ├── ticket.js ├── user.js ├── job.js ├── client.js └── transaction.js ├── package.json ├── users.json ├── README.md ├── routes ├── dashboard.js ├── clients.js ├── jobs.js ├── transactions.js ├── users.js └── tickets.js └── app.js /views/partials/tmp.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/scripts/barcharts.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ -------------------------------------------------------------------------------- /mongod: -------------------------------------------------------------------------------- 1 | mongod --nojournal 2 | -------------------------------------------------------------------------------- /public/scripts/history.js: -------------------------------------------------------------------------------- 1 | $(document).ready( function () { 2 | $('#myHistoryTable').DataTable(); 3 | } ); -------------------------------------------------------------------------------- /Project Images/Clients.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zernst3/my-simple-crm/HEAD/Project Images/Clients.png -------------------------------------------------------------------------------- /Project Images/Dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zernst3/my-simple-crm/HEAD/Project Images/Dashboard.png -------------------------------------------------------------------------------- /public/scripts/dropdown_search.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $('.dropdown_search').select2(); 3 | }); -------------------------------------------------------------------------------- /Project Images/Add Ticket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zernst3/my-simple-crm/HEAD/Project Images/Add Ticket.png -------------------------------------------------------------------------------- /Project Images/Edit Transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zernst3/my-simple-crm/HEAD/Project Images/Edit Transaction.png -------------------------------------------------------------------------------- /Project Images/User Permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zernst3/my-simple-crm/HEAD/Project Images/User Permissions.png -------------------------------------------------------------------------------- /Project Images/Client Information.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zernst3/my-simple-crm/HEAD/Project Images/Client Information.png -------------------------------------------------------------------------------- /views/partials/footer.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /functions/isLoggedIn.js: -------------------------------------------------------------------------------- 1 | // Confirm Logged In User 2 | 3 | function isLoggedIn(req, res, next){ 4 | if(req.isAuthenticated()){ 5 | return next(); 6 | } 7 | req.flash("error", "Please Login"); 8 | res.redirect("/login"); 9 | } 10 | 11 | module.exports = isLoggedIn; -------------------------------------------------------------------------------- /functions/isAdministrator.js: -------------------------------------------------------------------------------- 1 | // Confirm User is Administrator 2 | 3 | function isAdministrator(req, res, next){ 4 | if(req.user.user_permissions === "Administrator"){ 5 | return next(); 6 | } else { 7 | res.send("Permission Denied"); 8 | } 9 | } 10 | 11 | module.exports = isAdministrator; -------------------------------------------------------------------------------- /functions/numberWithCommas.js: -------------------------------------------------------------------------------- 1 | // Source: https://stackoverflow.com/questions/2901102/how-to-print-a-number-with-commas-as-thousands-separators-in-javascript 2 | function numberWithCommas(x) { 3 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 4 | } 5 | 6 | module.exports = numberWithCommas; -------------------------------------------------------------------------------- /public/scripts/confirm_password.js: -------------------------------------------------------------------------------- 1 | function confirmPassword(e){ 2 | if (document.getElementById("password").value != document.getElementById("confirm_password").value){ 3 | e.preventDefault(); 4 | alert("Passwords Do Not Match"); 5 | return false; 6 | } 7 | return true; 8 | } -------------------------------------------------------------------------------- /public/scripts/sidebar.js: -------------------------------------------------------------------------------- 1 | //Vertical navbar source: https://bootstrapious.com/p/bootstrap-vertical-navbar 2 | 3 | $(function() { 4 | // Sidebar toggle behavior 5 | $('#sidebarCollapse').on('click', function() { 6 | $('#sidebar, #content, .toggle-button').toggleClass('active'); 7 | }); 8 | }); -------------------------------------------------------------------------------- /functions/isManager.js: -------------------------------------------------------------------------------- 1 | // Confirm User is Manager or above 2 | 3 | function isManager(req, res, next){ 4 | if(req.user.user_permissions === "Manager" || req.user.user_permissions === "Administrator"){ 5 | return next(); 6 | } else { 7 | res.send("Permission Denied"); 8 | } 9 | } 10 | 11 | module.exports = isManager; -------------------------------------------------------------------------------- /views/not_found.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('partials/header'); %> 2 | 404 Not Found 3 | 4 | <%- include ('partials/navbar'); %> 5 | 6 |

404

7 |

Not Found

8 | 9 | <%- include ('partials/footer'); %> -------------------------------------------------------------------------------- /functions/isAdministratorOrCurrentUser.js: -------------------------------------------------------------------------------- 1 | // Confirm User is Administrator 2 | 3 | function isAdministratorOrCurrentUser(req, res, next){ 4 | if(req.user.user_permissions === "Administrator" || req.user.id === req.params.id){ 5 | return next(); 6 | } else { 7 | res.send("Permission Denied"); 8 | } 9 | } 10 | 11 | module.exports = isAdministratorOrCurrentUser; -------------------------------------------------------------------------------- /public/styles/page_layout.css: -------------------------------------------------------------------------------- 1 | .page_title { 2 | text-align: center; 3 | font-weight: bold; 4 | } 5 | 6 | .page_margin { 7 | margin-top: 10%; 8 | margin-bottom: 10%; 9 | } 10 | 11 | .form_layout { 12 | max-height: 100%; 13 | border: 1px solid black; 14 | padding: 25px; 15 | margin: auto; 16 | text-align: center; 17 | color: black; 18 | max-width: 960px; 19 | position: relative; 20 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); 21 | } 22 | 23 | body { 24 | max-width: 100%; 25 | } -------------------------------------------------------------------------------- /public/styles/my_account.css: -------------------------------------------------------------------------------- 1 | 2 | #my_account { 3 | max-height: 100%; 4 | border: 1px solid black; 5 | padding: 25px; 6 | margin: auto; 7 | text-align: left; 8 | color: black; 9 | width: 50%; 10 | position: relative; 11 | top: 50%; 12 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); 13 | } 14 | 15 | #edit_info { 16 | max-height: 100%; 17 | border: 1px solid black; 18 | padding: 25px; 19 | margin: auto; 20 | margin-top: 2rem; 21 | text-align: left; 22 | color: black; 23 | width: 50%; 24 | position: relative; 25 | top: 50%; 26 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); 27 | } -------------------------------------------------------------------------------- /public/styles/login.css: -------------------------------------------------------------------------------- 1 | form { 2 | max-width: 65%; 3 | margin: auto; 4 | } 5 | 6 | .form-control { 7 | /* width: 65%; */ 8 | margin: auto; 9 | } 10 | 11 | #register { 12 | padding-top: 1rem; 13 | } 14 | 15 | a { 16 | color: rgb(8, 7, 105); 17 | } 18 | 19 | .button { 20 | font-size: 1.5rem; 21 | color: white; 22 | background:rgba(63, 144, 158, 0.4); 23 | padding: 10px; 24 | border: 2px solid white; 25 | border-radius: 10px; 26 | } 27 | 28 | .top_margin { 29 | margin-top: 57px; 30 | } 31 | 32 | 33 | 34 | p { 35 | margin-top: 0rem; 36 | margin-bottom: 0.5rem; 37 | } 38 | 39 | .button { 40 | margin-bottom: 1rem; 41 | } -------------------------------------------------------------------------------- /public/styles/information_card.css: -------------------------------------------------------------------------------- 1 | p { 2 | margin: 0; 3 | } 4 | 5 | .title { 6 | font-weight: bold; 7 | } 8 | 9 | .card_content { 10 | max-height: 100%; 11 | padding: 25px; 12 | margin: auto; 13 | margin-top: 2rem; 14 | margin-bottom: 2rem; 15 | color: black; 16 | width: 100%; 17 | max-width: 1000px; 18 | position: relative; 19 | font-size: 1.2em; 20 | border: 1px solid rgba(0,0,0,.125); 21 | border-radius: 10px; 22 | background-color: #fff; 23 | } 24 | 25 | .block_info { 26 | padding: 1.5rem; 27 | } 28 | 29 | .card_content form { 30 | display: inline; 31 | } 32 | 33 | .stripe_1 { 34 | background-color: white; 35 | } 36 | 37 | .stripe_2 { 38 | background-color: rgb(212, 212, 212); 39 | } -------------------------------------------------------------------------------- /models/ticket.js: -------------------------------------------------------------------------------- 1 | const Transaction = require ("./transaction"); 2 | User = require ("./user"); 3 | Ticket = require ("./ticket"); 4 | Job = require ("./job"); 5 | Client = require ("./client"); 6 | 7 | mongoose = require("mongoose"); 8 | 9 | // =======================Ticket Schema 10 | 11 | var ticketSchema = new mongoose.Schema({ 12 | ticket_name: String, 13 | description: String, 14 | created_by: {type: mongoose.Schema.Types.ObjectID, ref: "User"}, 15 | assigned_user: {type: mongoose.Schema.Types.ObjectID, ref: "User"}, 16 | completed_by_user: {type: mongoose.Schema.Types.ObjectID, ref: "User"}, 17 | due_date: {type: Date}, 18 | completed_date: {type: Date}, 19 | completed_description: String, 20 | date_added: {type: Date} 21 | }); 22 | 23 | module.exports = mongoose.model("Ticket", ticketSchema); -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | const Transaction = require ("./transaction"); 2 | User = require ("./user"); 3 | Ticket = require ("./ticket"); 4 | Job = require ("./job"); 5 | Client = require ("./client"); 6 | 7 | mongoose = require("mongoose"); 8 | passportLocalMongoose = require("passport-local-mongoose"); 9 | 10 | // =======================User Schema 11 | 12 | var userSchema = new mongoose.Schema({ 13 | first_name: String, 14 | middle_name: String, 15 | last_name: String, 16 | username: String, 17 | email_address: String, 18 | phone_number: String, 19 | street: String, 20 | city: String, 21 | state: String, 22 | zip: String, 23 | password: String, 24 | user_permissions: String, 25 | date_added: {type: Date, default: Date.now} 26 | }); 27 | 28 | userSchema.plugin(passportLocalMongoose); 29 | 30 | module.exports = mongoose.model("User", userSchema); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my_simple_crm", 3 | "version": "1.0.0", 4 | "description": "A simple Inventory web application.", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node app.js" 9 | }, 10 | "author": "Zachary Ernst", 11 | "license": "ISC", 12 | "dependencies": { 13 | "body-parser": "^1.19.0", 14 | "connect-flash": "^0.1.1", 15 | "connect-mongodb-session": "^2.3.3", 16 | "dotenv": "^8.2.0", 17 | "ejs": "^3.1.3", 18 | "express": "^4.17.1", 19 | "express-session": "^1.17.1", 20 | "memorystore": "^1.6.2", 21 | "method-override": "^3.0.0", 22 | "moment": "^2.26.0", 23 | "mongoose": "^5.9.14", 24 | "nodemailer": "^6.4.8", 25 | "passport": "^0.4.1", 26 | "passport-local": "^1.0.0", 27 | "passport-local-mongoose": "^6.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /public/styles/items.css: -------------------------------------------------------------------------------- 1 | #add_remove { 2 | padding-bottom: 10px; 3 | font-size: 1.2rem; 4 | } 5 | 6 | .item_table { 7 | padding-left: 5%; 8 | padding-right: 5%; 9 | } 10 | 11 | .fa-exclamation-circle { 12 | color: red; 13 | } 14 | 15 | .fa-exclamation-triangle { 16 | color: yellow; 17 | } 18 | 19 | .fa-plus { 20 | color: green; 21 | } 22 | 23 | .fa-minus { 24 | color: red; 25 | } 26 | 27 | .fa-check { 28 | color: green; 29 | } 30 | 31 | .key { 32 | padding-bottom: 5px; 33 | } 34 | 35 | .key .icon { 36 | padding-right: 5px; 37 | } 38 | 39 | .item_small_table { 40 | max-height: 100%; 41 | padding: 10px; 42 | margin: auto; 43 | margin-top: 2rem; 44 | margin-bottom: 2rem; 45 | color: black; 46 | max-width: 90%; 47 | position: relative; 48 | font-size: 1.2em; 49 | border: 1px solid rgba(0,0,0,.125); 50 | border-radius: 10px; 51 | } -------------------------------------------------------------------------------- /models/job.js: -------------------------------------------------------------------------------- 1 | const Transaction = require ("./transaction"); 2 | User = require ("./user"); 3 | Ticket = require ("./ticket"); 4 | Job = require ("./job"); 5 | Client = require ("./client"); 6 | 7 | mongoose = require("mongoose"); 8 | 9 | // =======================Job Schema 10 | 11 | var jobSchema = new mongoose.Schema({ 12 | job_name: String, 13 | created_by: {type: mongoose.Schema.Types.ObjectID, ref: "User"}, 14 | street: String, 15 | city: String, 16 | state: String, 17 | zip: String, 18 | description: String, 19 | billing_price: mongoose.Decimal128, 20 | client: {type: mongoose.Schema.Types.ObjectID, ref: "Client"}, 21 | transactions: [{type: mongoose.Schema.Types.ObjectID, ref: "Transaction"}], 22 | start_date: {type: Date}, 23 | end_date: {type: Date}, 24 | date_added: {type: Date, default: Date.now} 25 | }); 26 | 27 | module.exports = mongoose.model("Job", jobSchema); -------------------------------------------------------------------------------- /models/client.js: -------------------------------------------------------------------------------- 1 | const Transaction = require ("./transaction"), 2 | User = require ("./user"), 3 | Ticket = require ("./ticket"), 4 | Job = require ("./job"), 5 | Client = require ("./client"), 6 | 7 | mongoose = require("mongoose"); 8 | 9 | // =======================Client Schema 10 | 11 | var clientSchema = new mongoose.Schema({ 12 | organization_name: String, 13 | first_name: String, 14 | middle_name: String, 15 | last_name: String, 16 | email_address: String, 17 | phone_number: String, 18 | street: String, 19 | city: String, 20 | state: String, 21 | zip: String, 22 | description: String, 23 | active: {type: Boolean, deafult: true}, 24 | date_added: {type: Date}, 25 | created_by: {type: mongoose.Schema.Types.ObjectID, ref: "User"}, 26 | transactions: [{type: mongoose.Schema.Types.ObjectID, ref: "Transaction"}], 27 | jobs: [{type: mongoose.Schema.Types.ObjectID, ref: "Job"}] 28 | }); 29 | 30 | module.exports = mongoose.model("Client", clientSchema); -------------------------------------------------------------------------------- /models/transaction.js: -------------------------------------------------------------------------------- 1 | // Schemas 2 | const Transaction = require ("./transaction"); 3 | User = require ("./user"); 4 | Ticket = require ("./ticket"); 5 | Job = require ("./job"); 6 | Client = require ("./client"); 7 | 8 | mongoose = require("mongoose"); 9 | 10 | 11 | // =======================Transaction Schema 12 | 13 | var transactionSchema = new mongoose.Schema({ 14 | job: {type: mongoose.Schema.Types.ObjectId, ref: Job}, 15 | client: {type: mongoose.Schema.Types.ObjectId, ref: Client}, 16 | deposited_by_user: {type: mongoose.Schema.Types.ObjectId, ref: User}, 17 | transaction_info:{ 18 | associated_name: String, 19 | amount: mongoose.Decimal128, 20 | method: String, 21 | receipt_number: String, 22 | date: {type: Date} 23 | }, 24 | billing_address: { 25 | street: String, 26 | city: String, 27 | state: String, 28 | zip: String 29 | }, 30 | notes: String, 31 | date_added: {type: Date} 32 | }); 33 | 34 | module.exports = mongoose.model("Transaction", transactionSchema); -------------------------------------------------------------------------------- /views/users/users.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Users 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |

Users

7 | 8 | 9 |
10 | 11 | Create New UserUser Permissions 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <% users.forEach(function(user){ %> 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | <% }) %> 37 | 38 | 39 | 40 |
Username:First Name:Middle Name:Last Name:User Permissions:
<%= user.username %><%= user.first_name %><%= user.middle_name %><%= user.last_name %><%= user.user_permissions %>
41 |
42 | 43 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /public/scripts/items.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | $.fn.dataTable.moment( 'dddd MMM D YYYY' ); 3 | 4 | $('table.displayTickets').DataTable({ 5 | "order": [[ 2, "asc" ]] 6 | }); 7 | } ); 8 | 9 | $(document).ready(function() { 10 | $('table.displayUsers').DataTable({ 11 | "order": [[ 0, "asc" ]] 12 | }); 13 | } ); 14 | 15 | $(document).ready(function() { 16 | $('table.displayClients').DataTable({ 17 | "order": [[ 0, "asc" ]] 18 | }); 19 | } ); 20 | 21 | $(document).ready(function() { 22 | $.fn.dataTable.moment( 'dddd MMM D YYYY' ); 23 | 24 | $('table.displayJobs').DataTable({ 25 | "order": [[ 1, "desc" ]] 26 | }); 27 | } ); 28 | 29 | $(document).ready(function() { 30 | $.fn.dataTable.moment( 'dddd MMM D YYYY' ); 31 | 32 | $('table.displayTransactions').DataTable({ 33 | "order": [[ 0, "desc" ]] 34 | }); 35 | } ); 36 | 37 | $(document).ready(function() { 38 | $.fn.dataTable.moment( 'dddd MMM D YYYY' ); 39 | 40 | $('table.displayAssociatedJobs').DataTable({ 41 | "order": [[ 2, "desc" ]] 42 | }); 43 | } ); 44 | 45 | $(document).ready(function() { 46 | $.fn.dataTable.moment( 'dddd MMM D YYYY' ); 47 | 48 | $('table.displayAssociatedTransactions').DataTable({ 49 | "order": [[ 0, "desc" ]] 50 | }); 51 | } ); -------------------------------------------------------------------------------- /views/login.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('partials/header'); %> 2 | 3 | Login 4 | 5 |
6 | 7 | 8 | <% if (error && error.length > 0) { %> 9 |
10 |
<%= error %>
11 |
12 | <% } %> 13 | <% if (success && success.length > 0) { %> 14 |
15 |
<%= success %>
16 |
17 | <% } %> 18 | <% if (info && info.length > 0) { %> 19 |
20 |
<%= info %>
21 |
22 | <% } %> 23 | 24 |
25 |

MY SIMPLE CRM

26 |

Login:

27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 | 40 | 41 | <%- include ('partials/footer'); %> -------------------------------------------------------------------------------- /views/jobs/jobs.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Jobs 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |

Jobs

7 | 8 | 9 |
10 | 11 | Add New Job 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <% jobs.forEach(function(job){ %> 25 | 26 | 27 | 28 | 29 | 30 | <% if (job["client"]) { %> 31 | <% if (job["client"].organization_name) { %> 32 | 33 | <% } else { %> 34 | 35 | <% } %> 36 | <% } else { %> 37 | 38 | <% } %> 39 | 40 | 41 | <% }) %> 42 | 43 | 44 | 45 |
Job Name:Start Date:End Date:Associated Client:
<%= job.job_name %><%= job.start_date.toDateString() %><%= job.end_date.toDateString() %><%= job["client"].organization_name %><%= job["client"].last_name, %> <%= job["client"].last_name %>, <%= job["client"].middle_name, %>[deleted]
46 |
47 | 48 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/users/edit_user_password.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit User Password 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Edit Password for <%= user.username %>:

9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 | 32 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /users.json: -------------------------------------------------------------------------------- 1 | [{ 2 | "_id": { 3 | "$oid": "5ed1ad3480c6d420a53d6a10" 4 | }, 5 | "username": "Admin", 6 | "first_name": "", 7 | "middle_name": "", 8 | "last_name": "", 9 | "email_address": "", 10 | "phone_number": "", 11 | "street": "", 12 | "city": "", 13 | "state": "", 14 | "zip": "", 15 | "user_permissions": "Administrator", 16 | "date_added": { 17 | "$date": "2020-05-30T00:47:48.560Z" 18 | }, 19 | "salt": "9b6e531653ecaa104a5b42ef91fc0cb76380e70909ef5b19d7ee4f0b1c3b0cb2", 20 | "hash": "6629d0155ea6201d0fde7a95307f620d29aee027401d1fe0566a7612e2cb6d95b3fc0f6fee6693dea456095bea766236f09dffe1c917873f830aa482eedcf227015f1fc7ce1f05e9ac3dd6fed7fcea1d7655e0f601d0e35950ba95a8bb069bd1c246842a682ca27f3c7cca20aec21d1a89f96a26e4dc00e2c9dcd6a39c732691a5f3af92a780919b7ad4a3eebb63d61ab2faa3d8e00f47dd145f7bcfbf5f60f3933d6e4f9a54aeda1eb484d1d271a3df37a50866f75e0f6198a7cbe98280d1fbad0efcce9dcefc06040287ef19360631e3930e2578c922caa219abe5314cf0df3e532da9cff460caf4768d0e61352ed379045d668a16034a8d8e56df4e94709ec3f40ce0ca35f77982ca9e16aae6e58305613f39eb9159450bc55cc2d79176c468af13971565333be3f0f0c25fcae15b0c7b02293d4fe32a85551ef38f65d6d88a480c8ea370a2b2b207de77f07685cb122bc730e4893285b28b248e67ba20326aa461e489ace93c0e1ff4e6fb15ee943bc77bb85435323b704f676d68f19c73a3e25db38a15389517449e0253f95655dd4d7ee0d998e523eba146bf1c1cac68cba888e2c924efd0061def585cd128c4670236ddb419595a4ea90214073f85a60f1db49925e38f5cfd8d169418ca1f3b977e6ae7198246be49515293a1e1ff695c8d4ca2b276ce5d2b50c3ac720204818d6fb307184a5613414b491315f1b53d", 21 | "__v": 0 22 | }] -------------------------------------------------------------------------------- /public/styles/dashboard.css: -------------------------------------------------------------------------------- 1 | .custom_card { 2 | padding: 2%; 3 | border-radius: 15px; 4 | min-width: 15rem; 5 | } 6 | 7 | .row { 8 | padding-right: 5%; 9 | padding-left: 5%; 10 | margin-right: 0px; 11 | margin-left: 0px; 12 | } 13 | 14 | .card_padding { 15 | padding-bottom: 2%; 16 | } 17 | 18 | .custom_card i { 19 | padding-bottom: 1rem; 20 | } 21 | 22 | #myTransactionsChart { 23 | min-height: 15rem; 24 | min-width: 18rem; 25 | } 26 | 27 | .card-title { 28 | font-size: 1.5rem; 29 | } 30 | 31 | .card-body { 32 | text-align: center; 33 | } 34 | 35 | .card_text { 36 | font-weight: bold; 37 | color: rgb(255, 255, 255); 38 | } 39 | 40 | .card_text_bottom { 41 | font-size: 2rem; 42 | margin-bottom: 0rem; 43 | } 44 | 45 | .tickets { 46 | background-color: rgba(62, 91, 112); 47 | } 48 | 49 | .clients { 50 | background-color: rgba(64, 112, 62); 51 | } 52 | 53 | .jobs { 54 | background-color: rgba(112, 62, 62); 55 | } 56 | 57 | .transactions { 58 | background-color: rgb(165, 157, 47); 59 | } 60 | 61 | .dropdown_parent { 62 | position: relative; 63 | } 64 | 65 | .dropdown, .dropdown1 { 66 | padding: 0%; 67 | display: none; 68 | position: relative; 69 | width: 100%; 70 | max-height: 0%; 71 | font-size: 100%; 72 | text-align: center; 73 | border-radius: 5px; 74 | transition: all .9s; 75 | } 76 | 77 | .dropdown_on { 78 | padding: 1%; 79 | display: block; 80 | transition-property: all; 81 | transition-duration: 1s; 82 | z-index: 0; 83 | } 84 | 85 | .monthly_income, .monthly_jobs { 86 | margin-top: 2rem; 87 | margin-left: 1rem; 88 | position: relative; 89 | } -------------------------------------------------------------------------------- /views/tickets/complete_ticket.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Complete Ticket 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Complete Ticket: <%= ticket.ticket_name %>

9 |
10 |
11 |

Completed By User:

12 | 22 |
23 |
24 | 25 |
26 | 27 |
28 |

Completed Description:

29 | 30 |
31 | 32 | 33 |
34 |
35 |
36 | 37 | 38 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/clients/clients.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Clients 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |

Clients

7 | 8 | 9 |
10 | 11 | Add New Client 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <% clients.forEach(function(client){ %> 26 | 27 | 28 | <% if (client.organization_name) { %> 29 | 30 | <% if (client.last_name) { %> 31 | 32 | <% } else { %> 33 | 34 | <% } %> 35 | <% } else { %> 36 | 37 | 38 | <% } %> 39 | 40 | 41 | 48 | 49 | 50 | <% }) %> 51 | 52 | 53 | 54 |
Organization/Client Last Name:Contact Name:Jobs:Transactions:Is Active:
<%= client.organization_name %><%= client.last_name %>, <%= client.first_name %> <%= client.middle_name %><%= client.last_name %><%= client.last_name %>, <%= client.first_name %> <%= client.middle_name %><%= client["jobs"].length %><%= client["transactions"].length %> 42 | <% if (client.active) { %> 43 | Yes 44 | <% } else { %> 45 | No 46 | <% } %> 47 |
55 |
56 | 57 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/tickets/tickets.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Tickets 3 | <%- include ('../partials/navbar'); %> 4 | 5 |

Tickets

6 | 7 |
8 | 9 | Create New Ticket 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% tickets.forEach(function(ticket){ %> 24 | 25 | 26 | 27 | <% if (Date.now() < ticket.due_date && !ticket.completed_date) { %> 28 | 29 | <% } %> 30 | <% if (Date.now() > ticket.due_date && !ticket.completed_date) { %> 31 | 32 | <% } %> 33 | <% if (ticket.completed_date) { %> 34 | 35 | <% } %> 36 | 37 | <% if (ticket.due_date) { %> 38 | 39 | <% } %> 40 | <% if (ticket.completed_date) { %> 41 | 42 | <% } else { %> 43 | 44 | <% } %> 45 | 46 | 47 | <% }) %> 48 | 49 | 50 | 51 |
Ticket Name:Ticket Status:Date Added:Due date:Date Completed:
<%= ticket.ticket_name %> Not Yet Completed Past Due Completed<%= ticket.date_added.toDateString() %><%= ticket.due_date.toDateString() %><%= ticket.completed_date.toDateString() %>
52 |
53 | 54 | 55 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/tickets/add_ticket.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Add Ticket 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Add Ticket:

9 |
10 |
11 | 12 |
13 |
14 |

Created By User:

15 | 20 |
21 |
22 |

Assign to User:

23 | 28 |
29 |
30 |

Due Date:

31 | 32 |
33 | 34 | 35 |
36 |

Ticket Description:

37 | 38 |
39 | 40 | 41 |
42 |
43 |
44 | 45 | 46 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/transactions/transactions.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Transactions 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |

Transactions

7 | 8 | 9 |
10 | <% if (currentUser.user_permissions === "Manager" || currentUser.user_permissions === "Administrator") { %> 11 | Add New Transaction 12 | <% } %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | <% transactions.forEach(function(transaction){ %> 27 | 28 | 29 | 30 | <% if (transaction["client"]) { %> 31 | <% if (transaction["client"].organization_name) { %> 32 | 33 | <% } else { %> 34 | 41 | <% } %> 42 | <% } else { %> 43 | 44 | <% } %> 45 | <% if (transaction["job"]) { %> 46 | 47 | <% } else { %> 48 | 49 | <% } %> 50 | 51 | 52 | 53 | 54 | <% }) %> 55 | 56 | 57 | 58 |
Deposited Date:Client Name:Associated Job:Date Added:Transaction Amount:
<%= transaction["transaction_info"]["date"].toDateString() %><%= transaction["client"].organization_name %> 35 | <%= transaction["client"].last_name %>, 36 | <% if (transaction["client"].middle_name) { %> 37 | <%= transaction["client"].middle_name %> 38 | <% } %> 39 | <%= transaction["client"].first_name %> 40 | [deleted]<%= transaction["job"].job_name %>[deleted]<%= transaction.date_added.toDateString() %>$<%= transaction["transaction_info"]["new_amount"] %>
59 |
60 | 61 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My Simple CRM 2 | A simple Web-based Client Relationship Management application that uses back-end and front-end technologies. I created this as an ambitious final project for CS50x at Harvard University. 3 | 4 | 5 | ## Table of Contents 6 | * [General Info](#general-info) 7 | * [Technologies](#technologies) 8 | * [Setup](#setup) 9 | 10 | ## General Info 11 | This is a simple CRM application that uses a Node server and MongoDB database to display various information to a small business about their client relations. Features of this application include: 12 | * Three different User Permission Levels 13 | * An emailing system that uses nodemailer to send emails to users for user information and tickets 14 | * Associated databases for easy relationship management 15 | * Datatables and charts.js on the front-end for easy visual views of information 16 | * Mobile-friendly design 17 | 18 | ## Technologies 19 | This project was created with 20 | * Back-End: 21 | * Node.js: 12.18.0 22 | * Body-parser: 1.19.0 23 | * Connect-Flash: 0.1.1 24 | * Connect-mongodb-session: 2.3.3 25 | * Dotenv: 8.2.0 26 | * ejs: 3.1.3 27 | * Express: 4.17.1 28 | * Express-Session: 1.17.1 29 | * Memorystore: 1.6.2 30 | * Method_Override: 3.0.0 31 | * Moment: 2.26.0 32 | * Mongoose: 5.9.14 33 | * Nodemailer: 6.4.8 34 | * Passport: 0.4.1 35 | * Passport-Local: 1.0.0 36 | * Passport-Local-Mongoose: 6.0.1 37 | 38 | * Front-End: 39 | * Bootstrap: 4.3.1 40 | * Chart.js: 2.8.0 41 | * Datatables: 1.10.20 42 | * Font-Awesome: 4.7.0 43 | * Jquery: 1.12.1 44 | * Moment: 2.8.4 45 | 46 | ## Setup 47 | To run this application locally, first you must have node.js and mongoDB installed. One installed, you must copy the "users" json documents into a mongo Database called "crm_app" in order to be able to login initially as an Admin. Make sure your application is connected to the database; I did this using Azure in Microsoft Visual Studio Code. To start, the username is "Admin" and the password is "Password;" these should be changed upon first starting this application. In the root application folder, create an env file that includes the email address and password for a gmail account to be associated with nodemailer with this specification: 48 | 49 | EMAIL= example@gmail.com 50 | PASSWORD= "**********" 51 | 52 | ``` 53 | $ cd ../my_simple_crm 54 | $ npm install 55 | $ node app.js 56 | ``` -------------------------------------------------------------------------------- /views/tickets/edit_ticket.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit Ticket 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Edit Ticket:

9 |
10 |
11 | 12 |
13 |
14 |

Created By User:

15 | 25 |
26 |
27 |

Assign to User:

28 | 38 |
39 |

Due Date:

40 |
41 | 42 |
43 | 44 |
45 |

Ticket Description:

46 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | 54 | 55 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /public/styles/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: rgb(241, 241, 241); 3 | } 4 | 5 | .zoom { 6 | transition: transform .2s; 7 | 8 | } 9 | 10 | .zoom:hover { 11 | transform: scale(1.1); 12 | z-index:1; 13 | } 14 | 15 | 16 | .software_name { 17 | color: white; 18 | } 19 | 20 | body { 21 | font-family: 'Quattrocento Sans', sans-serif; 22 | } 23 | 24 | .billing { 25 | display: inline-block; 26 | } 27 | 28 | hr { 29 | width: 90%; 30 | border: 0; 31 | height: 1px; 32 | background: #333; 33 | background-image: linear-gradient(to right, #ccc, #333, #ccc); 34 | } 35 | 36 | /* ==================Buttons================== */ 37 | 38 | .edit { 39 | display: inline-block; 40 | background-color: rgba(62, 91, 112); 41 | border: 1px solid transparent; 42 | border-radius: 50rem; 43 | margin: 0.5rem; 44 | margin-left: 0rem; 45 | padding: 10px; 46 | box-shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important; 47 | font-family: inherit; 48 | font-weight: bold; 49 | color: white; 50 | width: 200px; 51 | } 52 | 53 | .edit_complete { 54 | display: inline-block; 55 | background-color: rgba(62, 91, 112); 56 | border: 1px solid transparent; 57 | border-radius: 50rem; 58 | margin: 0.5rem; 59 | margin-left: 0rem; 60 | padding: 10px; 61 | box-shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important; 62 | font-family: inherit; 63 | font-weight: bold; 64 | color: white; 65 | width: 200px; 66 | } 67 | 68 | .delete { 69 | display: inline-block; 70 | background-color: rgb(141, 72, 72); 71 | border: 1px solid transparent; 72 | border-radius: 50rem; 73 | margin: 0.5rem; 74 | margin-left: 0rem; 75 | padding: 10px; 76 | box-shadow: 0 .125rem .25rem rgba(0,0,0,.075)!important; 77 | font-family: inherit; 78 | font-weight: bold; 79 | color: white; 80 | width: 200px; 81 | } 82 | 83 | .form_content { 84 | max-height: 100%; 85 | border: 1px solid black; 86 | padding: 25px; 87 | margin: auto; 88 | text-align: center; 89 | color: black; 90 | max-width: 750px; 91 | position: relative; 92 | box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.5); 93 | } 94 | 95 | label { 96 | margin-left: 0; 97 | font-size: 0.8em; 98 | } 99 | 100 | textarea { 101 | height: 150px; 102 | width: 100%; 103 | } 104 | 105 | .alert { 106 | text-align: center; 107 | font-weight: bold; 108 | } -------------------------------------------------------------------------------- /views/users/user.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | User Information 3 | 4 | <%- include ('../partials/navbar'); %> 5 | 6 |

User Information

7 |
8 |
9 |

Name:

10 |

<%= user.first_name %> <%= user.middle_name %> <%= user.last_name %>

11 |
12 |
13 |
14 |
15 |

Username:

16 |

<%= user.username %>

17 |
18 |
19 |
20 |
21 |

User Permissions:

22 |

<%= user.user_permissions %>

23 |
24 |
25 |
26 |
27 |
28 |
29 |

Email Address:

30 |

<%= user.email_address %>

31 |
32 |
33 |
34 |
35 |

Phone Number:

36 |

<%= user.phone_number %>

37 |
38 |
39 |
40 |
41 |
42 |
43 |

Address:

44 |
45 |

<%= user.street %>

46 |

<% if (user.city) { %> 47 | <%= user.city %>, 48 | <% } %> 49 | <%= user.state %> <%= user.zip %>

50 |
51 |
52 |
53 |
54 |
55 |

Created:

56 |

<%= user.date_added.toDateString() %>

57 |
58 |
59 |
60 | <% if (currentUser.user_permissions === "Administrator" || currentUser.username === user.username) { %> 61 |
62 | 63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 | <% } %> 71 |
72 | 73 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /views/dashboard.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('partials/header'); %> 2 | Dashboard 3 | 4 | <%- include ('partials/navbar'); %> 5 | 6 | 7 |

Dashboard

8 |
9 | 10 | 11 |
12 |
13 |
14 | 15 |
16 |

Tickets

17 |

Total number of outstanding tickets:

18 |

<%= outstanding_tickets %>

19 |
20 |
21 |
22 |
23 | 24 | 25 |
26 |
27 |
28 | 29 |
30 |

Clients

31 |

Clients Added This Month:

32 |

<%= clients_added %>

33 |
34 |
35 |
36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 |

Jobs

45 |

Jobs Added This Month:

46 |

<%= jobs_added %>

47 |
48 |
49 |
50 |
51 | 52 | 53 |
54 |
55 |
56 | 57 |
58 |

Transactions

59 |

Income YTD:

60 |

$<%= ytd_income %>

61 |
62 |
63 |
64 |
65 |
66 | 67 |
68 |
69 |
70 |
71 | 72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 | <%- include ('../public/scripts/charts'); %> 86 | 87 | <%- include ('partials/footer'); %> -------------------------------------------------------------------------------- /public/scripts/charts.ejs: -------------------------------------------------------------------------------- 1 | 45 | 46 | 75 | 76 | -------------------------------------------------------------------------------- /routes/dashboard.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | router = express.Router(); 3 | 4 | // Schemas 5 | const Transaction = require ("../models/transaction"), 6 | Ticket = require ("../models/ticket"), 7 | Job = require ("../models/job"), 8 | Client = require ("../models/client"); 9 | 10 | // Functions 11 | let numberWithCommas = require("../functions/numberWithCommas"); 12 | let isLoggedIn = require("../functions/isLoggedIn"); 13 | 14 | router.use(function(req, res, next){ 15 | res.locals.currentUser = req.user; 16 | next(); 17 | }); 18 | 19 | // =======================Dashboard 20 | 21 | router.get("/", isLoggedIn, async function(req, res){ 22 | let current_year = (new Date(Date.now())).getYear(); 23 | let current_month = (new Date(Date.now())).getMonth(); 24 | let transactions = [], 25 | clients = [], 26 | jobs = [], 27 | tickets = []; 28 | let outstanding_tickets = 0; 29 | let transaction_data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 30 | let jobs_data = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; 31 | let clients_added = 0; 32 | let jobs_added = 0; 33 | let ytd_income = 0; 34 | 35 | try { 36 | const foundTransactions = await Transaction.find({}).populate("job").populate("client").populate("deposited_by_user").exec(); 37 | for (let y = 0; y < foundTransactions.length; y++){ 38 | if (foundTransactions[y]["transaction_info"].date.getYear() == current_year){ 39 | ytd_income = Number(ytd_income) + Number(foundTransactions[y]["transaction_info"].amount); 40 | } 41 | } 42 | ytd_income = numberWithCommas(ytd_income); 43 | 44 | for (let i = 0; i < foundTransactions.length; i++){ 45 | foundTransactions[i]["transaction_info"]["new_amount"] = numberWithCommas(foundTransactions[i]["transaction_info"]["amount"]); 46 | } 47 | transactions = foundTransactions; 48 | 49 | clients = await Client.find({}).populate("transactions").populate("jobs").exec(); 50 | tickets = await Ticket.find({}).populate("created_by").populate("assigned_user").populate("completed_by_user").exec(); 51 | jobs = await Job.find({}).populate("created_by").populate("client").populate("transactions").exec(); 52 | 53 | 54 | for (let n = 0; n < tickets.length; n++){ 55 | if (tickets[n].completed_date == null){ 56 | outstanding_tickets = outstanding_tickets + 1; 57 | } 58 | } 59 | 60 | for (let x = 0; x < clients.length; x++){ 61 | if (clients[x].date_added.getMonth() == current_month){ 62 | clients_added = clients_added + 1; 63 | } 64 | } 65 | 66 | for (let r = 0; r < jobs.length; r++){ 67 | if (jobs[r].date_added.getMonth() == current_month){ 68 | jobs_added = jobs_added + 1; 69 | } 70 | } 71 | 72 | // Setting transactions per month: 73 | for (let t = 0; t < foundTransactions.length; t++){ 74 | for (let m = 0; m < 12; m++){ 75 | if (foundTransactions[t]["transaction_info"].date.getYear() == current_year && foundTransactions[t]["transaction_info"].date.getMonth() == m){ 76 | transaction_data[m] = Number(transaction_data[m]) + Number(foundTransactions[t]["transaction_info"].amount); 77 | } 78 | } 79 | } 80 | 81 | // Setting jobs per month: 82 | for (let j = 0; j < jobs.length; j++){ 83 | for (let m = 0; m < 12; m++){ 84 | if (jobs[j].start_date.getYear() == current_year && jobs[j].start_date.getMonth() == m){ 85 | jobs_data[m] = Number(jobs_data[m]) + 1; 86 | } 87 | } 88 | } 89 | } 90 | catch (err) {console.log(err);} 91 | 92 | res.render("dashboard", {ytd_income: ytd_income, clients_added: clients_added, outstanding_tickets: outstanding_tickets, jobs_added: jobs_added, transaction_data: transaction_data, jobs_data: jobs_data}); 93 | }); 94 | 95 | module.exports = router; -------------------------------------------------------------------------------- /public/styles/navbars.css: -------------------------------------------------------------------------------- 1 | 2 | /* ====================================================== */ 3 | /* Vertical navbar source: https://bootstrapious.com/p/bootstrap-vertical-navbar */ 4 | 5 | .vertical-nav { 6 | padding-top: 57px; 7 | width: 13rem; 8 | max-height: 100%; 9 | position: fixed; 10 | top: 0; 11 | left: 0; 12 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.1); 13 | transition: all 0.4s; 14 | background-color: rgba(214, 214, 214, 0.4); 15 | z-index: 1; 16 | height: 100vh; 17 | } 18 | 19 | #sidebartoggle { 20 | padding-right: 1rem; 21 | } 22 | 23 | @media (max-height: 960px) { 24 | .vertical-nav { 25 | overflow-y: scroll; 26 | } 27 | } 28 | 29 | #sidebarCollapse.active { 30 | max-width: 100%; 31 | margin: 0; 32 | transition: all 0.4s; 33 | } 34 | 35 | #sidebarCollapse { 36 | max-width: 100%; 37 | margin: 0; 38 | transition: all 0.4s; 39 | background-color: rgb(230, 230, 230); 40 | } 41 | 42 | #sidebartoggle { 43 | z-index:1; 44 | } 45 | 46 | 47 | #sidebar.active { 48 | margin-left: -13rem; 49 | } 50 | 51 | .page-content { 52 | margin-top: 57px; 53 | padding-top: 2%; 54 | padding-bottom: 2%; 55 | max-width: calc(100% - 13rem); 56 | margin-left: 13rem; 57 | transition: all 0.4s; 58 | } 59 | 60 | #content.active { 61 | max-width: 100%; 62 | margin-left: 0; 63 | margin-right: 0; 64 | } 65 | 66 | .vertical-nav .mobile_links { 67 | display: none; 68 | color: white !important; 69 | } 70 | 71 | /* For keeping the navbar active on desktop */ 72 | @media (min-width: 769px){ 73 | #sidebarCollapse { 74 | display: none; 75 | } 76 | 77 | .vertical-nav.active { 78 | padding-top: 57px; 79 | width: 13rem; 80 | max-height: 100%; 81 | position: fixed; 82 | top: 0; 83 | left: 0; 84 | box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.1); 85 | transition: all 0.4s; 86 | background-color: rgba(214, 214, 214, 0.4); 87 | z-index: 1; 88 | height: 100vh; 89 | } 90 | #content.active { 91 | margin-top: 57px; 92 | padding-top: 2%; 93 | padding-bottom: 2%; 94 | max-width: calc(100% - 13rem); 95 | margin-left: 13rem; 96 | transition: all 0.4s; 97 | } 98 | #sidebar.active { 99 | margin-left: 0rem; 100 | } 101 | } 102 | 103 | @media (max-width: 993px) { 104 | .vertical-nav .mobile_links { 105 | display: block; 106 | } 107 | } 108 | 109 | @media (max-width: 768px) { 110 | 111 | #content { 112 | width: 100%; 113 | margin-left: 0; 114 | margin-right: 0; 115 | margin-top: 57px; 116 | max-width: 100%; 117 | } 118 | 119 | .vertical-nav{ 120 | background-color: rgba(214, 214, 214, 0.8); 121 | } 122 | 123 | #sidebartoggle { 124 | padding-right: 0px; 125 | } 126 | 127 | } 128 | 129 | /* ====================================================== */ 130 | 131 | .navbar { 132 | background-color:rgba(6, 16, 34, 1); 133 | z-index: 2; 134 | } 135 | 136 | .nav-item { 137 | text-align: left; 138 | } 139 | 140 | .navbar_search_button { 141 | color: white; 142 | background-color: black; 143 | } 144 | 145 | .navbar_search_button:hover { 146 | color: white; 147 | background-color: black; 148 | } 149 | 150 | .vertical-nav .nav-item { 151 | margin: 0 auto; 152 | width: 70%; 153 | padding-top: 5%; 154 | padding-bottom: 5%; 155 | } 156 | 157 | .vertical-nav .nav-item .icon { 158 | text-align:center; 159 | align-items: center; 160 | justify-content: center; 161 | display: inline-block; 162 | width: 100%; 163 | } 164 | 165 | .vertical-nav .nav-item .nav-link { 166 | border: white; 167 | background:rgba(63, 144, 158, 0.6); 168 | border-radius: 25px; 169 | text-align: center; 170 | } 171 | 172 | .Welcome { 173 | color: black; 174 | } 175 | 176 | -------------------------------------------------------------------------------- /routes/clients.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | router = express.Router(); 3 | 4 | // Schemas 5 | const Transaction = require ("../models/transaction"); 6 | User = require ("../models/user"); 7 | Ticket = require ("../models/ticket"); 8 | Job = require ("../models/job"); 9 | Client = require ("../models/client"); 10 | 11 | // Functions 12 | let numberWithCommas = require("../functions/numberWithCommas"); 13 | let isLoggedIn = require("../functions/isLoggedIn"); 14 | let isAdministrator = require("../functions/isAdministrator"); 15 | let isManager = require("../functions/isManager"); 16 | 17 | router.use(function(req, res, next){ 18 | res.locals.currentUser = req.user; 19 | next(); 20 | }); 21 | 22 | // ========================================================================== index 23 | router.get("/", isLoggedIn, async function(req, res){ 24 | let clients = []; 25 | try { 26 | clients = await Client.find({}).populate("transactions").populate("jobs").exec() 27 | } 28 | catch (err) {console.log(err);} 29 | res.render("clients/clients", {clients: clients}); 30 | }); 31 | 32 | // ========================================================================== new 33 | router.get("/add", isLoggedIn, function(req, res){ 34 | res.render("clients/add_client"); 35 | }) 36 | 37 | // ========================================================================== show 38 | router.get("/:id", isLoggedIn, async function(req, res){ 39 | let client_info = {}; 40 | try { 41 | client_info = await Client.findById(req.params.id).populate("transactions").populate("jobs").populate("created_by").exec(); 42 | for (let i = 0; i < client_info.transactions.length; i++){ 43 | client_info.transactions[i]["transaction_info"]["new_amount"] = numberWithCommas(client_info.transactions[i]["transaction_info"]["amount"]); 44 | } 45 | } 46 | catch (err) {console.log(err);} 47 | res.render("clients/client_info", {client_info: client_info}); 48 | }); 49 | 50 | // ========================================================================== edit 51 | router.get("/:id/edit", isLoggedIn, isManager, async function(req, res){ 52 | client_info = {}; 53 | try { 54 | client_info = await Client.findById(req.params.id).populate("transactions").populate("jobs").exec(); 55 | } 56 | catch (err) {console.log(err);} 57 | res.render("clients/edit_client", {client_info: client_info}); 58 | }); 59 | 60 | // ========================================================================== create 61 | router.post("/", isLoggedIn, async function(req, res){ 62 | let client; 63 | try { 64 | client = req.body.client; 65 | client.date_added = moment(moment(Date.now()).format("YYYY-MM-DD")); 66 | client.created_by = req.user.id; 67 | client.active = true; 68 | await Client.create(client); 69 | if (client.organization_name) { 70 | console.log("Client " + "'" + client.organization_name + "'" + " has been created"); 71 | } else { 72 | console.log("Client " + "'" + client.last_name + ", " + client.first_name + "'" + " has been created"); 73 | } 74 | } 75 | catch (err) {console.log(err);} 76 | req.flash("success", "New client has been created"); 77 | res.redirect("/clients"); 78 | }); 79 | 80 | // ========================================================================== update 81 | router.put("/:id", isLoggedIn, isManager, async function(req, res){ 82 | let client = req.body.client 83 | try { 84 | await Client.findByIdAndUpdate(req.params.id, client); 85 | if (client.organization_name) { 86 | console.log("Client " + "'" + client.organization_name + "'" + " has been updated"); 87 | } else { 88 | console.log("Client " + "'" + client.last_name + ", " + client.first_name + "'" + " has been updated"); 89 | } 90 | } 91 | catch (err) {console.log(err);} 92 | req.flash("info", "Client has been updated"); 93 | res.redirect("/clients/" + req.params.id); 94 | }); 95 | 96 | // ========================================================================== delete 97 | router.delete("/:id", isLoggedIn, isAdministrator, async function(req, res){ 98 | try { 99 | await Client.findByIdAndRemove(req.params.id); 100 | console.log("Client has been deleted"); 101 | } 102 | catch (err) {console.log(err);} 103 | req.flash("error", "Client has been deleted"); 104 | res.redirect("/clients"); 105 | }); 106 | 107 | module.exports = router; -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // ========================================================Start of Header============================================================ 2 | const bodyParser = require("body-parser"), 3 | methodOverride = require("method-override"), 4 | mongoose = require("mongoose"), 5 | express = require("express"), 6 | flash = require("connect-flash"), 7 | app = express(); 8 | 9 | // Authentication 10 | const passport = require("passport"), 11 | LocalStrategy = require("passport-local"); 12 | 13 | // Schemas 14 | const Transaction = require ("./models/transaction"), 15 | User = require ("./models/user"), 16 | Ticket = require ("./models/ticket"), 17 | Job = require ("./models/job"), 18 | Client = require ("./models/client"); 19 | 20 | //tell express to serve public directory 21 | app.use(express.static(__dirname + "/public")); 22 | 23 | //tell express to look for ejs files 24 | app.set("view engine", "ejs"); 25 | 26 | // use bodyParser 27 | app.use(bodyParser.urlencoded({extended: true})); 28 | 29 | // use methodOverride 30 | app.use(methodOverride("_method")); 31 | 32 | // mongoose.connect("mongodb://localhost/crm_app"); 33 | 34 | require('dotenv').config(); 35 | 36 | mongoose.connect(process.env.MongoDB, { 37 | userNewUrlParser: true, 38 | useCreateIndex: true, 39 | }).then (() => { 40 | console.log("Connected to the database"); 41 | }).catch(err => { 42 | console.log("Error", err.message); 43 | }); 44 | 45 | // Optional. Use this if you create a lot of connections and don't want 46 | // to copy/paste `{ useNewUrlParser: true }`. 47 | mongoose.set('useNewUrlParser', true); 48 | mongoose.set('useUnifiedTopology', true); 49 | mongoose.set('useFindAndModify', false); 50 | 51 | // Mongoose Session 52 | const session = require("express-session"); 53 | const MongoDBStore = require("connect-mongodb-session")(session); 54 | const store = new MongoDBStore({ 55 | uri: process.env.MongoDB, 56 | collection: 'mySessions' 57 | }); 58 | 59 | // Catch errors 60 | store.on('error', function(error) { 61 | console.log(error); 62 | }); 63 | 64 | // Use Express Session 65 | app.use(session ({ 66 | secret: "You my friend, I will defend, and if we change, well, I love you anyway", 67 | cookie: { 68 | maxAge: 1000 * 60 * 60 * 24 * 7 // 1 week 69 | }, 70 | store: store, 71 | resave: true, 72 | saveUninitialized: true 73 | })); 74 | 75 | // Use Passport 76 | app.use(passport.initialize()); 77 | app.use(passport.session()); 78 | 79 | passport.use(new LocalStrategy(User.authenticate())); 80 | passport.serializeUser(User.serializeUser()); 81 | passport.deserializeUser(User.deserializeUser()); 82 | 83 | // Use Flash 84 | app.use(flash()); 85 | 86 | // Use Current Logged in Information and Flashing Information 87 | app.use(function(req, res, next){ 88 | res.locals.currentUser = req.user; 89 | res.locals.error = req.flash("error"); 90 | res.locals.success = req.flash("success"); 91 | res.locals.info = req.flash("info"); 92 | next(); 93 | }); 94 | 95 | // Functions 96 | let isLoggedIn = require("./functions/isLoggedIn"); 97 | 98 | // ========================================================End of Header============================================================== 99 | 100 | // Routes 101 | const users = require("./routes/users"), 102 | tickets = require("./routes/tickets"), 103 | clients = require("./routes/clients"), 104 | jobs = require("./routes/jobs"), 105 | transactions = require("./routes/transactions"); 106 | dashboard = require("./routes/dashboard"); 107 | 108 | app.use("/users", users); 109 | app.use("/tickets", tickets); 110 | app.use("/clients", clients); 111 | app.use("/jobs", jobs); 112 | app.use("/transactions", transactions); 113 | app.use(dashboard); 114 | 115 | // =======================Login/Register 116 | 117 | app.get("/login", function(req, res){ 118 | res.render("login"); 119 | }); 120 | 121 | app.post("/login", passport.authenticate("local", { 122 | successRedirect:"/", 123 | failureRedirect: "/login", 124 | failureFlash: true 125 | }), function(req, res){}); 126 | 127 | // =======================Logout 128 | app.get("/logout", function(req, res){ 129 | req.logout(); 130 | req.flash("success", "Logged Out"); 131 | res.redirect("/login"); 132 | }); 133 | 134 | // =======================Error 135 | 136 | app.get("*", isLoggedIn, function(req, res){ 137 | res.render("not_found"); 138 | }); 139 | 140 | // =======================Server 141 | 142 | app.listen(process.env.PORT || 5000, function() { 143 | console.log('Server listening on port'); 144 | }); -------------------------------------------------------------------------------- /views/tickets/ticket.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Ticket Information 3 | 4 | <%- include ('../partials/navbar'); %> 5 | 6 |

Ticket Information

7 |
8 |
9 |
10 |

Ticket Name:

11 |
12 |
13 |

<%= ticket.ticket_name %>

14 |
15 |
16 |
17 |
18 |

Created By User:

19 |
20 |
21 |

22 | <% if (ticket.created_by) { %> 23 | <%= ticket["created_by"].username %> 24 | <% } else { %> 25 | [deleted] 26 | <% } %> 27 |

28 |
29 |
30 |
31 |
32 |

Assigned to:

33 |
34 |
35 |

36 | <% if (ticket.assigned_user) { %> 37 | <%= ticket["assigned_user"].username %> 38 | <% } else { %> 39 | [deleted] 40 | <% } %> 41 |

42 |
43 |
44 |
45 |
46 |

Status:

47 |
48 |
49 | <% if (Date.now() < ticket.due_date && !ticket.completed_date) { %> 50 |

Not Yet Completed

51 | <% } %> 52 | <% if (Date.now() > ticket.due_date && !ticket.completed_date) { %> 53 |

Past Due

54 | <% } %> 55 | <% if (ticket.completed_date) { %> 56 |

Completed by: 57 | <% if (ticket["completed_by_user"]) { %> 58 | <%= ticket["completed_by_user"].username %> 59 | <% } else { %> 60 | [deleted] 61 | <% } %> 62 | on

<%= ticket.completed_date.toDateString() %>

63 | <% } %> 64 |
65 |
66 |
67 |
68 |

Date Added:

69 |
70 |
71 |

<%= ticket.date_added.toDateString() %>

72 |
73 |
74 |
75 |
76 |

Due Date:

77 |
78 |
79 | <% if (ticket.due_date) { %> 80 |

<%= ticket.due_date.toDateString() %>

81 | <% } %> 82 |
83 |
84 |
85 |
86 |

Date Added:

87 |
88 |
89 |

<%= ticket.date_added.toDateString() %>

90 |
91 |
92 |
93 |
94 |

Description:

95 |
96 |
97 | <%= ticket.description %> 98 |
99 |
100 | 101 | <% if (ticket.completed_description){ %> 102 |
103 |
104 |

Completed Description:

105 |
106 |
107 |

<%= ticket.completed_description %>

108 |
109 |
110 | <% } %> 111 | 112 |
113 |
114 | <% if (currentUser.user_permissions === "Administrator" || currentUser.user_permissions === "Manager") { %> 115 |
116 | 117 |
118 | <% } %> 119 |
120 |
121 | <% if (!ticket.completed_date) { %> 122 |
123 | 124 |
125 | <% } %> 126 |
127 |
128 | <% if (currentUser.user_permissions === "Administrator") { %> 129 |
130 | 131 |
132 | <% } %> 133 |
134 |
135 |
136 | 137 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/partials/navbar.ejs: -------------------------------------------------------------------------------- 1 |
2 | 38 |
39 | 40 | 97 | 98 |
99 | 100 | 101 | <% if (error && error.length > 0) { %> 102 |
103 |
<%= error %>
104 |
105 | <% } %> 106 | <% if (success && success.length > 0) { %> 107 |
108 |
<%= success %>
109 |
110 | <% } %> 111 | <% if (info && info.length > 0) { %> 112 |
113 |
<%= info %>
114 |
115 | <% } %> 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /views/clients/add_client.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Add Client 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 | 7 |
8 |
9 |

Add Client:

10 |
11 |
12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 |
26 | 27 |
28 |
29 | 30 |
31 |
32 | 33 |
34 |
35 |
36 | 37 |
38 |
39 | 42 |
43 |
44 | 45 |
46 |
47 |
48 |

Client Description:

49 | 50 |
51 | 52 | 53 |
54 |
55 |
56 | 57 | 58 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/transactions/transaction.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Transaction Information 3 | 4 | <%- include ('../partials/navbar'); %> 5 | 6 |

Transaction Information

7 |
8 |
9 | 10 |
11 |
12 |

Client Name:

13 |
14 |
15 |

16 | <% if (transaction["client"]) { %> 17 | <% if (transaction["client"].organization_name) { %> 18 | "><%= transaction["client"].organization_name %> 19 | <% } else { %> 20 | "> 21 | <%= transaction["client"].last_name %>, 22 | <% if (transaction["client"].middle_name) { %> 23 | <%= transaction["client"].middle_name %> 24 | <% } %> 25 | <%= transaction["client"].first_name %> 26 | 27 | <% } %> 28 | <% } else { %> 29 | [deleted] 30 | <% } %> 31 |

32 |
33 |
34 |
35 |
36 |

Job Name:

37 |
38 |
39 | <% if (transaction["job"]) { %> 40 |

"><%= transaction["job"].job_name %>

41 | <% } else { %> 42 |

">

[deleted]

>a

43 | <% } %> 44 |
45 |
46 |
47 |
48 |

Name on Transaction:

49 |
50 |
51 |

52 | <% if (transaction["transaction_info"].associated_name) { %> 53 |

<%= transaction["transaction_info"].associated_name %>

54 | <% } %> 55 |

56 |
57 |
58 |
59 |
60 |

Amount:

61 |
62 |
63 |

$<%= price %>

64 |
65 |
66 |
67 |
68 |

Payment Method:

69 |
70 |
71 |

<%= transaction["transaction_info"]["method"] %>

72 |
73 |
74 |
75 |
76 |

Check/Receipt Number:

77 |
78 |
79 |

<%= transaction["transaction_info"]["receipt_number"] %>

80 |
81 |
82 |
83 |
84 |

Deposited Date:

85 |
86 |
87 |

<%= transaction["transaction_info"]["date"].toDateString() %>

88 |

Deposited by: 89 | <% if (transaction["deposited_by_user"]) { %> 90 | <%= transaction["deposited_by_user"].username %> 91 | <% } else { %> 92 | [deleted] 93 | <% } %> 94 |

95 |
96 |
97 |
98 |
99 |

Date Added:

100 |
101 |
102 |

<%= transaction.date_added.toDateString() %>

103 |
104 |
105 |
106 |
107 |

Billing Address:

108 |
109 |
110 |

<%= transaction["billing_address"]["street"] %>

111 |

<%= transaction["billing_address"]["city"] %>, <%= transaction["billing_address"]["state"] %> <%= transaction["billing_address"]["zip"] %>

112 |
113 |
114 |
115 |
116 |

Notes:

117 |
118 |
119 |

<%= transaction.notes %>

120 |
121 |
122 |
123 |
124 | <% if (currentUser.user_permissions === "Administrator") { %> 125 |
126 | 127 |
128 | <% } %> 129 |
130 |
131 | <% if (currentUser.user_permissions === "Administrator") { %> 132 |
133 | 134 |
135 | <% } %> 136 |
137 |
138 | 139 |
140 |
141 | 142 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/transactions/add_transaction.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Add Transaction 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Add Transaction:

9 |
10 |
11 |
12 |
13 |

Associated Job:

14 | 22 |
23 |
24 |
25 |
26 |

Associated Client:

27 | 41 |
42 |
43 |
44 |
45 |

Deposited By User:

46 | 52 |
53 |
54 |
55 | 56 |

Transaction Information:

57 |
58 | 59 |
60 |

Transaction Amount:

61 | $ 62 |
63 | 64 |
65 | 66 |
67 | 68 |
69 |
70 |

Payment Method:

71 | 78 |
79 |
80 |
81 |

Deposit Date:

82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 |

Check/Receipt Number:

90 | 91 |
92 | 93 |
94 |

Name on Transaction:

95 | 96 |
97 |
98 | 99 | 100 | 101 |

Billing Address:

102 |
103 | 104 |
105 |
106 |
107 | 108 |
109 |
110 | 111 |
112 |
113 | 114 |
115 |
116 | 117 |
118 |

Notes:

119 | 120 |
121 | 122 | 123 | 124 |
125 |
126 |
127 | 128 | 129 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/jobs/add_job.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Add Job 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Add Job:

9 |
10 |
11 | 12 |
13 |

Job Location:

14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 |
22 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |

Associated Client:

33 | 47 |
48 |
49 | 50 |
51 |
52 | 53 |
54 | 55 |
56 |
57 | $ 58 |
59 | 60 |
61 | 62 | 63 |
64 |
65 | 66 |
67 |

Job Description:

68 | 69 |
70 | 71 | 72 |
73 |
74 |
75 | 76 | 77 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/jobs/job.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Job Information 3 | 4 | <%- include ('../partials/navbar'); %> 5 | 6 |

Job Information

7 |
8 |
9 |
10 |
11 |
12 |

Job Name:

13 |
14 |
15 |

<%= job.job_name %>

16 |
17 |
18 |
34 |
35 |
36 |

Date Added:

37 |
38 |
39 |

<%= job.date_added.toDateString() %>

40 | <% if (job.created_by) { %> 41 |

By User: 42 | <% if (job.created_by) { %> 43 | <%= job["created_by"].username %> 44 | <% } else { %> 45 | [deleted] 46 | <% } %> 47 |

48 | <% } %> 49 |
50 |
51 |
52 |
53 |

Start Date:

54 |
55 |
56 |

<%= job.start_date.toDateString() %>

57 |
58 |
59 |
60 |
61 |

End Date:

62 |
63 |
64 |

<%= job.end_date.toDateString() %>

65 |
66 |
67 |
68 |
69 |

Date Added:

70 |
71 |
72 |

<%= job.date_added.toDateString() %>

73 | <% if (job.created_by) { %> 74 |

By User: 75 | <% if (job.created_by) { %> 76 | <%= job["created_by"].username %> 77 | <% } else { %> 78 | [deleted] 79 | <% } %> 80 |

81 | <% } %> 82 |
83 |
84 |
85 |
86 |

Address:

87 |
88 |
89 |

<%= job.street %>

90 |

<%= job.city %>, <%= job.state %> <%= job.zip %>

91 |
92 |
93 |
94 |
95 |

Billing Price:

96 |
97 |
98 |

$<%= price %>

99 | 100 |
101 |
102 |
103 |
104 |

Balance:

105 |
106 |
107 |

$<%= job.balance %>

108 |
109 |
110 |
111 |
112 |

Description:

113 |
114 |
115 |

116 | <%= job.description %> 117 |

118 |
119 |
120 |
121 |
122 | <% if (currentUser.user_permissions === "Manager" || currentUser.user_permissions === "Administrator") { %> 123 |
124 | 125 |
126 | <% } %> 127 |
128 |
129 | <% if (currentUser.user_permissions === "Administrator") { %> 130 |
131 | 132 |
133 | <% } %> 134 |
135 |
136 |
137 |
138 | 139 |
140 |
141 |

Associated Transactions:

142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | <% if (job.transactions) { %> 152 | <% job["transactions"].forEach(function(transaction){ %> 153 | 154 | 155 | 156 | 157 | 158 | 159 | <% }) %> 160 | <% } %> 161 | 162 | 163 |
Deposited Date:Transaction Amount:
<%= transaction["transaction_info"].date.toDateString() %>$<%= transaction["transaction_info"].new_amount %>
164 |
165 |
166 |
167 | 168 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/clients/client_info.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Client Information 3 | 4 | <%- include ('../partials/navbar'); %> 5 | 6 |

Client Information

7 |
8 |
9 |
10 | 11 |
12 |
13 |

Organization Name:

14 |
15 |
16 | <% if (client_info.organization_name) { %> 17 |

<%= client_info.organization_name %>

18 | <% } else { %> 19 |

<%= client_info.last_name %>, <%= client_info.first_name %> <%= client_info.middle_name %>

20 | <% } %> 21 |
22 |
23 |
24 |
25 |

Contact Name:

26 |
27 |
28 |

<%= client_info.first_name %> <%= client_info.middle_name %> <%= client_info.last_name %>

29 |
30 |
31 |
32 |
33 |

Email Address:

34 |
35 |
36 |

<%= client_info.email_address %>

37 |
38 |
39 |
40 |
41 |

Phone Number:

42 |
43 |
44 |

<%= client_info.phone_number %>

45 |
46 |
47 |
48 |
49 |

Address:

50 |
51 |
52 |

<%= client_info.street %>

53 |

<%= client_info.city %>, <%= client_info.state %> <%= client_info.zip %>

54 |
55 |
56 |
57 |
58 |

Date Added:

59 |
60 |
61 |

<%= client_info.date_added.toDateString() %>

62 |

By User: 63 | <% if (client_info.created_by) { %> 64 | <%= client_info.created_by.username %> 65 | <% } else { %> 66 | [deleted] 67 | <% } %> 68 |

69 |
70 |
71 |
72 |
73 |

Is Active:

74 |
75 |
76 |

77 | <% if (client_info.active) { %> 78 | true 79 | <% } else { %> 80 | false 81 | <% } %> 82 |

83 |
84 |
85 |
86 |
87 |

Description:

88 |
89 |
90 |

91 | <%= client_info.description %> 92 |

93 |
94 |
95 |
96 |
97 | <% if (currentUser.user_permissions === "Manager" || currentUser.user_permissions === "Administrator") { %> 98 |
99 | 100 |
101 | <% } %> 102 |
103 |
104 | <% if (currentUser.user_permissions === "Administrator") { %> 105 |
106 | 107 |
108 | <% } %> 109 |
110 |
111 |
112 |
113 | 114 |
115 | 116 |
117 |

Associated Jobs:

118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | <% client_info["jobs"].forEach(function(job){ %> 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | <% }) %> 138 | 139 | 140 |
Job Name:Job Start Date:Job End Date:
<%= job.job_name %><%= job.end_date.toDateString() %><%= job.start_date.toDateString() %>
141 |
142 | 143 |
144 |

Associated Transactions:

145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | <% client_info["transactions"].forEach(function(transaction){ %> 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | <% }) %> 165 | 166 | 167 |
Deposited Date:Date Added:Amount:
<%= transaction["transaction_info"]["date"].toDateString() %><%= transaction["date_added"].toDateString() %>$<%= transaction["transaction_info"]["new_amount"] %>
168 |
169 | 170 |
171 |
172 | 173 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/users/edit_user.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit User 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 | 7 | 8 |
9 |
10 |

Edit User: <%= user.username %>

11 |
12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 | 43 |
44 |
45 | 46 |
47 |
48 | 49 |
50 |

User Permissions:

51 | 57 |
58 | 59 |
60 |
61 |
62 | 63 | 64 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/users/add_user.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Add User 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |

Add User:

8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |

User Permissions:

56 | 61 |
62 | 63 |
64 |
65 | 66 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /routes/jobs.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | router = express.Router(), 3 | moment = require("moment"); 4 | 5 | // Schemas 6 | const User = require ("../models/user"); 7 | Ticket = require ("../models/ticket"); 8 | Job = require ("../models/job"); 9 | Client = require ("../models/client"); 10 | 11 | // Functions 12 | let numberWithCommas = require("../functions/numberWithCommas"); 13 | let isLoggedIn = require("../functions/isLoggedIn"); 14 | let isAdministrator = require("../functions/isAdministrator"); 15 | let isManager = require("../functions/isManager"); 16 | 17 | router.use(function(req, res, next){ 18 | res.locals.currentUser = req.user; 19 | next(); 20 | }); 21 | 22 | // ========================================================================== index 23 | router.get("/", isLoggedIn, async function(req, res){ 24 | let jobs = []; 25 | try { 26 | jobs = await Job.find({}).populate("created_by").populate("client").populate("transactions").exec(); 27 | } 28 | catch (err) {console.log(err);} 29 | res.render("jobs/jobs", {jobs: jobs}); 30 | }); 31 | 32 | // ========================================================================== new 33 | router.get("/add", isLoggedIn, async function(req, res){ 34 | let users = []; 35 | let clients = []; 36 | try { 37 | users = await User.find({}) 38 | clients = await Client.find({}) 39 | } 40 | catch (err) {console.log(err);} 41 | res.render("jobs/add_job", {users: users, clients: clients}); 42 | }); 43 | 44 | // ========================================================================== show 45 | router.get("/:id", isLoggedIn, async function(req, res){ 46 | let job = {}; 47 | let price; 48 | try { 49 | job = await Job.findById(req.params.id).populate("created_by").populate("client").populate("transactions").exec(); 50 | let balance = job.billing_price; 51 | // Add Price with commas 52 | if (job.transactions){ 53 | for (let i = 0; i < job["transactions"].length; i++){ 54 | job["transactions"][i]["transaction_info"]["new_amount"] = numberWithCommas(job["transactions"][i]["transaction_info"]["amount"]); 55 | } 56 | // Balance 57 | for (let i = 0; i < job["transactions"].length; i++){ 58 | balance = balance - job["transactions"][i]["transaction_info"]["amount"]; 59 | } 60 | } 61 | 62 | job.balance = await numberWithCommas(balance); 63 | price = await numberWithCommas(job.billing_price); 64 | } 65 | catch (err) {console.log(err);} 66 | res.render("jobs/job", {job: job, price: price}); 67 | }); 68 | 69 | // ========================================================================== edit 70 | router.get("/:id/edit", isLoggedIn, isManager, async function(req, res){ 71 | let job = {}; 72 | let users = []; 73 | let clients = []; 74 | let start_date; 75 | let end_date; 76 | 77 | try { 78 | job = await Job.findById(req.params.id).populate("created_by").populate("client").populate("transactions").exec(); 79 | users = await User.find({}); 80 | clients = await Client.find({}); 81 | start_date = moment(job.start_date).format("YYYY-MM-DD"); 82 | end_date = moment(job.end_date).format("YYYY-MM-DD"); 83 | } 84 | catch (err) {console.log(err);} 85 | res.render("jobs/edit_job", {job: job, users: users, clients: clients, start_date, end_date}); 86 | }); 87 | 88 | // ========================================================================== create 89 | router.post("/", isLoggedIn, async function(req, res){ 90 | let job = req.body.job; 91 | try { 92 | job.start_date = moment(job.start_date); 93 | job.end_date = moment(job.end_date); 94 | job.date_created = moment(moment(Date.now()).format("YYYY-MM-DD")); 95 | job.created_by = req.user.id; 96 | let jobCreated = await Job.create(job); 97 | console.log("Job " + "'" + job.job_name + "'" + " has been created"); 98 | 99 | // attach job to connected client 100 | let client = await Client.findById(req.body.job["client"]); 101 | await client.jobs.push(jobCreated._id); 102 | await client.save(); 103 | console.log('Job id added to client documents successfully'); 104 | } 105 | catch (err) {console.log(err);} 106 | req.flash("success", "New job has been created"); 107 | res.redirect("/jobs"); 108 | }); 109 | 110 | // ========================================================================== update 111 | router.put("/:id", isLoggedIn, isManager, async function(req, res){ 112 | try { 113 | // remove job from old conencted client 114 | await Client.updateMany({jobs: req.params.id}, 115 | {$pull: {jobs: {$in: [req.params.id]}} 116 | }); 117 | console.log('Job id removed from old client documents successfully'); 118 | 119 | let job = req.body.job; 120 | job.start_date = moment(job.start_date); 121 | job.end_date = moment(job.end_date); 122 | let foundJob = await Job.findByIdAndUpdate(req.params.id, job); 123 | console.log("Job " + "'" + job.job_name + "'" + " has been updated"); 124 | 125 | // attach job to new connected client 126 | let client = await Client.findById(req.body.job["client"]); 127 | client.jobs.push(foundJob._id); 128 | client.save(); 129 | console.log('Job id added to new client documents successfully'); 130 | } 131 | catch (err) {console.log(err);} 132 | req.flash("info", "Job has been updated"); 133 | res.redirect("/jobs/" + req.params.id); 134 | }); 135 | 136 | // ========================================================================== delete 137 | router.delete("/:id", isLoggedIn, isAdministrator, async function(req, res){ 138 | try { 139 | await Client.updateMany({ jobs: req.params.id },{ 140 | $pull: { jobs: {$in: [req.params.id]} } 141 | }); 142 | console.log('Job id removed from client documents successfully'); 143 | 144 | await Job.findByIdAndRemove(req.params.id); 145 | console.log('Job successfully removed'); 146 | } 147 | catch (err) {console.log(err);} 148 | req.flash("error", "Job has been deleted"); 149 | res.redirect("/jobs"); 150 | }); 151 | 152 | module.exports = router; -------------------------------------------------------------------------------- /views/jobs/edit_job.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit Job 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Edit Job:

9 |
10 |
11 | 12 |
13 | 14 |

Job Location:

15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |

Associated Client:

34 | 60 |
61 | 62 |
63 | 64 |
65 |
66 | 67 |
68 | 69 |
70 |
71 | $ 72 |
73 | 74 |
75 | 76 |
77 |
78 | 79 |
80 |

Job Description:

81 | 82 |
83 | 84 | 85 |
86 |
87 |
88 | 89 | 90 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/clients/edit_client.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit Client 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 | 7 |
8 |
9 |

Edit Client: 10 | <% if (client_info.organization_name) { %> 11 | <%= client_info.organization_name %> 12 | <% } else { %> 13 | <%= client_info.first_name %> <%= client_info.middle_name %> <%= client_info.last_name %> 14 | <% } %> 15 |

16 |
17 |
18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 | 26 |
27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 | 48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 |

Client Description:

56 | 57 |
58 |
59 |

Is Active:

60 | 65 |
66 | 67 |
68 | 69 | 70 |
71 |
72 |
73 | 74 | 75 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/users/user_permissions.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit User 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |

User Permissions

8 |
9 | 10 |
11 |
12 |

Users:

13 |
14 |
15 |

Users:

16 |
17 |
18 |

Can View Account Information

19 |

Can Change Password

20 |
21 |
22 |
23 |
24 |

Tickets:

25 |
26 |
27 |

Can View Tickets

28 |

Can Create Tickets

29 |

Can Complete Tickets

30 |
31 |
32 |
33 |
34 |

Clients:

35 |
36 |
37 |

Can View Clients

38 |

Can Create Clients

39 |
40 |
41 |
42 |
43 |

Jobs:

44 |
45 |
46 |

Can View Jobs

47 |

Can Create Jobs

48 |
49 |
50 |
51 |
52 |

Transactions:

53 |
54 |
55 |

Can View Transactions

56 |
57 |
58 |
59 |
60 | 61 | 62 |
63 |
64 |

Managers:

65 | 66 |
67 |
68 |

All User Permissions and:

69 |
70 |
71 |
72 |
73 |

Tickets:

74 |
75 |
76 |

Can Edit Tickets

77 |
78 |
79 |
80 |
81 |

Clients:

82 |
83 |
84 |

Can Edit Clients

85 |
86 |
87 |
88 |
89 |

Jobs:

90 |
91 |
92 |

Can Edit Jobs

93 |
94 |
95 |
96 |
97 |

Transactions:

98 |
99 |
100 |

Can Add Transactions

101 |
102 |
103 |
104 |
105 |
106 |
107 |

Aministrator:

108 | 109 |
110 |
111 |

All Manager Permissions and:

112 |
113 |
114 |
115 |
116 |

Can Perform All User Administration

117 |
118 |
119 |
120 |
121 |

Tickets:

122 |
123 |
124 |

Can Delete Tickets

125 |
126 |
127 |
128 |
129 |

Clients:

130 |
131 |
132 |

Can Delete Clients

133 |
134 |
135 |
136 |
137 |

Jobs:

138 |
139 |
140 |

Can Delete Jobs

141 |
142 |
143 |
144 |
145 |

Transactions:

146 |
147 |
148 |

Can Edit Transactions

149 |

Can Delete Transactions

150 |
151 |
152 |
153 |
154 |
155 | 156 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /views/transactions/edit_transaction.ejs: -------------------------------------------------------------------------------- 1 | <%- include ('../partials/header'); %> 2 | Edit Transaction 3 | <%- include ('../partials/navbar'); %> 4 | 5 | 6 |
7 |
8 |

Edit Transaction:

9 |
10 |
11 |
12 |
13 |

Associated Job:

14 | 30 |
31 |
32 |
33 |
34 |

Associated Client:

35 | 59 |
60 |
61 |
62 |
63 |

Deposited By User:

64 | 78 |
79 |
80 |
81 | 82 |

Transaction Information:

83 |
84 | 85 |
86 |

Transaction Amount:

87 | $ 88 |
89 | " type="number"> 90 |
91 | 92 |
93 | 94 |
95 |
96 |

Payment Method:

97 | 105 |
106 |
107 |
108 |

Deposit Date:

109 |
110 | 111 |
112 |
113 |
114 | 115 |
116 |
117 |

Check/Receipt Number:

118 | " type="text"> 119 |
120 |
121 |

Name on Transaction:

122 | " placeholder="ex. check/credit card name" type="text"> 123 |
124 |
125 | 126 |

Billing Address:

127 |
128 | " type="text"> 129 |
130 |
131 |
132 | " type="text"> 133 |
134 |
135 | " type="text"> 136 |
137 |
138 | " type="text"> 139 |
140 |
141 | 142 |
143 |

Notes:

144 | 145 |
146 | 147 | 148 |
149 |
150 |
151 | 152 | 153 | <%- include ('../partials/footer'); %> -------------------------------------------------------------------------------- /routes/transactions.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | router = express.Router(); 3 | moment = require("moment"); 4 | 5 | // Schemas 6 | const Transaction = require ("../models/transaction"); 7 | User = require ("../models/user"); 8 | Ticket = require ("../models/ticket"); 9 | Job = require ("../models/job"); 10 | Client = require ("../models/client"); 11 | 12 | // Functions 13 | let numberWithCommas = require("../functions/numberWithCommas"); 14 | let isLoggedIn = require("../functions/isLoggedIn"); 15 | let isAdministrator = require("../functions/isAdministrator"); 16 | let isManager = require("../functions/isManager"); 17 | 18 | router.use(function(req, res, next){ 19 | res.locals.currentUser = req.user; 20 | next(); 21 | }); 22 | 23 | // ========================================================================== index 24 | router.get("/", isLoggedIn, async function(req, res){ 25 | let transactions = []; 26 | try { 27 | transactions = await Transaction.find({}).populate("job").populate("client").populate("deposited_by_user").exec(); 28 | for (let i = 0; i < transactions.length; i++){ 29 | transactions[i]["transaction_info"]["new_amount"] = numberWithCommas(transactions[i]["transaction_info"]["amount"]); 30 | } 31 | } 32 | catch (err) {console.log(err);} 33 | res.render("transactions/transactions", {transactions: transactions}); 34 | }); 35 | 36 | // ========================================================================== new 37 | router.get("/add", isLoggedIn, isManager, async function(req, res){ 38 | let users = []; 39 | let clients = []; 40 | let jobs = []; 41 | try { 42 | users = await User.find({}); 43 | clients = await Client.find({}); 44 | jobs = await Job.find({}); 45 | } 46 | catch (err) {console.log(err);} 47 | res.render("transactions/add_transaction", {users: users, clients: clients, jobs: jobs}); 48 | }); 49 | 50 | // ========================================================================== show 51 | router.get("/:id", isLoggedIn, async function(req, res){ 52 | let transaction = {}; 53 | let price; 54 | try { 55 | transaction = await Transaction.findById(req.params.id).populate("job").populate("client").populate("deposited_by_user").exec(); 56 | price = await numberWithCommas(transaction["transaction_info"]["amount"]); 57 | } 58 | catch (err) {console.log(err);} 59 | res.render("transactions/transaction", {transaction: transaction, price: price}); 60 | }); 61 | 62 | // ========================================================================== edit 63 | router.get("/:id/edit", isLoggedIn, isAdministrator, async function(req, res){ 64 | let transaction = {}; 65 | let users = []; 66 | let clients = []; 67 | let jobs = []; 68 | let deposit_date; 69 | try { 70 | transaction = await Transaction.findById(req.params.id).populate("job").populate("client").populate("deposited_by_user").exec(); 71 | users = await User.find({}); 72 | clients = await Client.find({}); 73 | jobs = await Job.find({}); 74 | deposit_date = moment(transaction["transaction_info"]["date"]).format("YYYY-MM-DD"); 75 | } 76 | catch (err) {console.log(err);} 77 | res.render("transactions/edit_transaction", {transaction: transaction, users: users, clients: clients, jobs: jobs, deposit_date: deposit_date}); 78 | }); 79 | 80 | // ========================================================================== create 81 | router.post("/", isLoggedIn, isManager, async function(req, res){ 82 | try { 83 | let transaction = req.body.transaction; 84 | 85 | transaction["transaction_info"].date = moment(transaction["transaction_info"].date); 86 | transaction.date_added = moment(moment(Date.now()).format("YYYY-MM-DD")); 87 | let newTransaction = await Transaction.create(transaction); 88 | console.log("Transaction successfully created"); 89 | 90 | // attach transaction to connected job 91 | let job = await Job.findById(req.body.transaction["job"]); 92 | await job.transactions.push(newTransaction._id); 93 | await job.save(); 94 | console.log('transaction id added to job documents successfully'); 95 | 96 | // attach transaction to conencted client 97 | let client = await Client.findById(req.body.transaction["client"]); 98 | await client.transactions.push(newTransaction._id); 99 | await client.save(); 100 | console.log('transaction id added to client documents successfully'); 101 | } 102 | catch (err) {console.log(err);} 103 | req.flash("success", "New transaction has been created"); 104 | res.redirect("/transactions"); 105 | }); 106 | 107 | // ========================================================================== update 108 | router.put("/:id", isLoggedIn, isAdministrator, async function(req, res){ 109 | try { 110 | // remove transaction from old connected client 111 | await Client.updateMany({transactions: req.params.id}, 112 | {$pull: {transactions: {$in: [req.params.id]}} 113 | }); 114 | console.log("transaction id removed from old client documents successfully"); 115 | 116 | // remove transaction from old conencted job 117 | await Job.updateMany({transactions: req.params.id}, 118 | {$pull: {transactions: {$in: [req.params.id]}} 119 | }); 120 | console.log("transaction id removed from old job documents successfully"); 121 | 122 | let transaction = req.body.transaction; 123 | transaction["transaction_info"].date = moment(transaction["transaction_info"].date); 124 | let updatedTransaction = await Transaction.findByIdAndUpdate(req.params.id, transaction); 125 | console.log("Transaction successfully updated"); 126 | 127 | // attach transaction to new connected job 128 | let job = await Job.findById(req.body.transaction["job"]); 129 | await job.transactions.push(updatedTransaction._id); 130 | await job.save(); 131 | console.log('transaction id added to new job documents successfully'); 132 | 133 | // attach transaction to new conencted client 134 | let client = await Client.findById(req.body.transaction["client"]); 135 | await client.transactions.push(updatedTransaction._id); 136 | await client.save(); 137 | console.log('transaction id added to client documents successfully'); 138 | } 139 | catch (err) {console.log(err);} 140 | req.flash("info", "Transaction has been updated"); 141 | res.redirect("/transactions/" + req.params.id); 142 | }); 143 | 144 | // ========================================================================== delete 145 | router.delete("/:id", isLoggedIn, isAdministrator, async function(req, res){ 146 | try { 147 | // remove transaction from connected client 148 | await Client.updateMany({transactions: req.params.id}, 149 | {$pull: {transactions: {$in: [req.params.id]}} 150 | }); 151 | console.log("transaction id removed from client documents successfully"); 152 | 153 | // remove transaction from conencted job 154 | await Job.updateMany({transactions: req.params.id}, 155 | {$pull: {transactions: {$in: [req.params.id]}} 156 | }); 157 | console.log("transaction id removed from job documents successfully"); 158 | 159 | // delete job 160 | await Transaction.findByIdAndRemove(req.params.id); 161 | console.log("transaction has been deleted"); 162 | } 163 | catch (err) {console.log(err);} 164 | req.flash("error", "Transaction has been deleted"); 165 | res.redirect("/transactions"); 166 | }); 167 | 168 | module.exports = router; -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | router = express.Router(); 3 | 4 | // Schemas 5 | const User = require ("../models/user"); 6 | Ticket = require ("../models/ticket"); 7 | Job = require ("../models/job"); 8 | Client = require ("../models/client"); 9 | 10 | // Functions 11 | let isLoggedIn = require("../functions/isLoggedIn"); 12 | let isAdministrator = require("../functions/isAdministrator"); 13 | let isAdministratorOrCurrentUser = require("../functions/isAdministratorOrCurrentUser"); 14 | 15 | router.use(function(req, res, next){ 16 | res.locals.currentUser = req.user; 17 | next(); 18 | }); 19 | 20 | require('dotenv').config(); 21 | const nodemailer = require('nodemailer'); 22 | 23 | // 1. Transporter 24 | let transporter = nodemailer.createTransport({ 25 | service: 'gmail', 26 | auth: { 27 | user: process.env.EMAIL, 28 | pass: process.env.PASSWORD 29 | } 30 | }); 31 | 32 | // ========================================================================== index 33 | router.get("/", isLoggedIn, isAdministrator, async function(req, res){ 34 | users = []; 35 | try { 36 | users = await User.find({}); 37 | } 38 | catch (err) {console.log(err);} 39 | res.render("users/users", {users: users}); 40 | }); 41 | 42 | // ========================================================================== User Permissions 43 | router.get("/user_permissions", isLoggedIn, isAdministrator, async function(req, res){ 44 | res.render("users/user_permissions"); 45 | }); 46 | 47 | // ========================================================================== new 48 | router.get("/add", isLoggedIn, isAdministrator, function(req, res){ 49 | res.render("users/add_user"); 50 | }) 51 | 52 | // ========================================================================== show 53 | router.get("/:id", isLoggedIn, isAdministratorOrCurrentUser, async function(req, res){ 54 | user = {}; 55 | try { 56 | user = await User.findById(req.params.id); 57 | } 58 | catch (err) {console.log(err);} 59 | return res.render("users/user", {user: user}); 60 | }); 61 | 62 | // ========================================================================== edit 63 | router.get("/:id/edit", isLoggedIn, isAdministratorOrCurrentUser, async function(req, res){ 64 | user = {}; 65 | try { 66 | user = await User.findById(req.params.id); 67 | } 68 | catch (err) {console.log(err);} 69 | res.render("users/edit_user", {user: user}); 70 | }); 71 | 72 | // ========================================================================== create 73 | router.post("/", isLoggedIn, isAdministrator, async function(req, res){ 74 | let users = []; 75 | let user = {}; 76 | let newUser = new User({ 77 | username: req.body.user["username"], 78 | first_name: req.body.user["first_name"], 79 | middle_name: req.body.user["middle_name"], 80 | last_name: req.body.user["last_name"], 81 | email_address: req.body.user["email_address"], 82 | phone_number: req.body.user["phone_number"], 83 | street: req.body.user["street"], 84 | city: req.body.user["city"], 85 | state: req.body.user["state"], 86 | zip: req.body.user["zip"], 87 | user_permissions: req.body.user["user_permissions"], 88 | }); 89 | 90 | users = await User.find({ username: { $eq: newUser.username }}); 91 | if (users.length > 0){ 92 | console.log("Error: User Already Exists"); 93 | req.flash("error", "Username already exists") 94 | return res.redirect("/users/add"); 95 | } 96 | 97 | // If username does not exist 98 | try { 99 | user = await User.register(newUser, req.body.user["password"]); 100 | console.log("New user " + "'" + newUser.username + "'" + " created"); 101 | } 102 | catch (err) {console.log(err);} 103 | 104 | // Email New User 105 | try { 106 | // 2. 107 | let mailOptions = { 108 | from: 'zernst.crm.app@gmail.com', 109 | to: user.email_address, 110 | subject: 'New User ' + '"' + user.username + '"' + ' has been created', 111 | html: "

New User

" + 112 | "

Username: " + user.username + "

" + 113 | "

Permissions: " + user.user_permissions + "

" 114 | } 115 | 116 | // 3. 117 | transporter.sendMail(mailOptions, function(err, data) { 118 | if (err){ 119 | console.log(err); 120 | } else { 121 | console.log("Email Sent Successfully"); 122 | } 123 | }); 124 | } 125 | catch (err) {console.log(err);} 126 | 127 | req.flash("success", "New user has been created"); 128 | res.redirect("/users"); 129 | }); 130 | 131 | // ========================================================================== update 132 | router.put("/:id", isLoggedIn, isAdministratorOrCurrentUser, async function(req, res){ 133 | let user = {}; 134 | 135 | try { 136 | user = await User.findByIdAndUpdate(req.params.id, req.body.user); 137 | console.log("User " + "'" + req.body.user["username"] + "'" + " has been updated") 138 | } 139 | catch (err) {console.log(err);} 140 | 141 | // Email User Update 142 | try { 143 | // 2. 144 | let mailOptions = { 145 | from: 'zernst.crm.app@gmail.com', 146 | to: user.email_address, 147 | subject: 'User ' + '"' + user.username + '"' + ' has been updated', 148 | html: "

User

" + 149 | "

Username: " + user.username + "

" + 150 | "

Permissions: " + user.user_permissions + "

" 151 | } 152 | 153 | // 3. 154 | transporter.sendMail(mailOptions, function(err, data) { 155 | if (err){ 156 | console.log(err); 157 | } else { 158 | console.log("Email Sent Successfully"); 159 | } 160 | }); 161 | } 162 | catch (err) {console.log(err);} 163 | 164 | req.flash("info", "User has been updated"); 165 | res.redirect("/users/" + req.params.id); 166 | }); 167 | 168 | // ========================================================================== delete 169 | router.delete("/:id", isLoggedIn, isAdministratorOrCurrentUser, async function(req, res){ 170 | try { 171 | await User.findByIdAndRemove(req.params.id); 172 | console.log("User has been deleted"); 173 | } 174 | catch (err) {console.log(err);} 175 | req.flash("error", "User has been deleted"); 176 | res.redirect("/users"); 177 | }); 178 | 179 | // ========================================================================== edit password 180 | router.get("/:id/edit_password", isLoggedIn, isAdministratorOrCurrentUser, async function(req, res){ 181 | user = {}; 182 | try { 183 | user = await User.findById(req.params.id); 184 | } 185 | catch (err) {console.log(err);} 186 | res.render("users/edit_user_password", {user: user}); 187 | }); 188 | 189 | // ========================================================================== update password 190 | router.put("/:id/edit_password", isLoggedIn, isAdministratorOrCurrentUser, async function(req, res){ 191 | user = {}; 192 | try { 193 | user = await User.findById(req.params.id) 194 | } 195 | catch (err) {console.log(err);} 196 | 197 | user.authenticate(req.body.user["old_password"], async function(err, model, passwordError){ 198 | if(passwordError){ 199 | req.flash("error", "Old password entered was incorrect"); 200 | res.redirect("/users/" + req.params.id + "/edit_password"); 201 | } else if (model) { 202 | try { 203 | await user.changePassword(req.body.user["old_password"], req.body.user["new_password"]); 204 | console.log("User " + "'" + user.username + "'" + " password has been changed"); 205 | } 206 | catch (err) {console.log(err);} 207 | 208 | // Email User Update 209 | try { 210 | // 2. 211 | let mailOptions = { 212 | from: 'zernst.crm.app@gmail.com', 213 | to: user.email_address, 214 | subject: 'Password has been changed for ' + '"' + user.username + '"', 215 | html: "

User

" + 216 | "

Username: " + user.username + "

" + 217 | "

Permissions: " + user.user_permissions + "

" 218 | } 219 | 220 | // 3. 221 | transporter.sendMail(mailOptions, function(err, data) { 222 | if (err){ 223 | console.log(err); 224 | } else { 225 | console.log("Email Sent Successfully"); 226 | } 227 | }); 228 | } 229 | catch (err) {console.log(err);} 230 | 231 | req.flash("info", "User password been updated"); 232 | res.redirect("/users/" + req.params.id); 233 | } else 234 | { 235 | console.log(err); 236 | res.redirect("/users/" + req.params.id); 237 | } 238 | }); 239 | }); 240 | 241 | module.exports = router; -------------------------------------------------------------------------------- /public/styles/the-datepicker.css: -------------------------------------------------------------------------------- 1 | .the-datepicker__container .the-datepicker__main { 2 | border-top-left-radius: 0; 3 | } 4 | .the-datepicker__container.the-datepicker__container--over .the-datepicker__main { 5 | border-top-left-radius: 0.3em; 6 | border-bottom-left-radius: 0; 7 | } 8 | .the-datepicker__container.the-datepicker__container--left .the-datepicker__main { 9 | border-top-left-radius: 0.3em; 10 | border-top-right-radius: 0; 11 | } 12 | .the-datepicker__container.the-datepicker__container--over.the-datepicker__container--left .the-datepicker__main { 13 | border-bottom-left-radius: 0.3em; 14 | border-top-right-radius: 0.3em; 15 | border-bottom-right-radius: 0; 16 | } 17 | .the-datepicker__main { 18 | background-color: #fff; 19 | border: 1px solid #ccc; 20 | border-radius: 0.3em; 21 | padding: 0.4em; 22 | font-family: Arial, Helvetica, sans-serif; 23 | line-height: 1em; 24 | box-sizing: border-box; 25 | overflow: hidden; 26 | } 27 | .the-datepicker__main .the-datepicker__body { 28 | margin-top: 1em; 29 | } 30 | .the-datepicker__main th.the-datepicker__week-day { 31 | width: 2.1em; 32 | min-width: 2.1em; 33 | max-width: 2.1em; 34 | padding: 0.5em 0; 35 | text-align: center; 36 | font-weight: normal; 37 | text-transform: uppercase; 38 | color: #666; 39 | font-size: 0.85em; 40 | } 41 | .the-datepicker__main a.the-datepicker__button { 42 | display: block; 43 | width: 2.1em; 44 | min-width: 2.1em; 45 | max-width: 2.1em; 46 | padding: 0.6em 0; 47 | text-decoration: none; 48 | text-align: center; 49 | color: #007eff; 50 | border-radius: 0.3em; 51 | } 52 | .the-datepicker__main a.the-datepicker__button:hover { 53 | background-color: #d1e8ff; 54 | text-decoration: none; 55 | } 56 | .the-datepicker__main .the-datepicker__title { 57 | display: inline-block; 58 | width: 70%; 59 | } 60 | .the-datepicker__main .the-datepicker__title-content { 61 | display: inline-block; 62 | padding: 0.5em; 63 | } 64 | .the-datepicker__main .the-datepicker__control { 65 | display: inline-block; 66 | width: 30%; 67 | text-align: right; 68 | } 69 | .the-datepicker__main .the-datepicker__reset, 70 | .the-datepicker__main .the-datepicker__close { 71 | display: inline-block; 72 | font-weight: bold; 73 | } 74 | .the-datepicker__main .the-datepicker__navigation { 75 | margin-top: 0.3em; 76 | } 77 | .the-datepicker__main .the-datepicker__go { 78 | display: inline-block; 79 | width: 15%; 80 | } 81 | .the-datepicker__main .the-datepicker__go a.the-datepicker__button { 82 | display: inline-block; 83 | font-weight: bold; 84 | } 85 | .the-datepicker__main .the-datepicker__go-next { 86 | text-align: right; 87 | } 88 | .the-datepicker__main .the-datepicker__state { 89 | display: inline-block; 90 | width: 70%; 91 | } 92 | .the-datepicker__main .the-datepicker__month { 93 | display: inline-block; 94 | width: 62%; 95 | text-align: center; 96 | } 97 | .the-datepicker__main .the-datepicker__year { 98 | display: inline-block; 99 | width: 38%; 100 | text-align: center; 101 | } 102 | .the-datepicker__main .the-datepicker__month-year { 103 | text-align: center; 104 | } 105 | .the-datepicker__main select.the-datepicker__select { 106 | font-size: 0.9em; 107 | margin: 0; 108 | padding: 0; 109 | } 110 | .the-datepicker__main table.the-datepicker__calendar { 111 | display: table; 112 | border-collapse: collapse; 113 | } 114 | .the-datepicker__main table.the-datepicker__calendar thead { 115 | display: table-header-group; 116 | } 117 | .the-datepicker__main table.the-datepicker__calendar tbody { 118 | display: table-row-group; 119 | } 120 | .the-datepicker__main table.the-datepicker__calendar tr { 121 | display: table-row; 122 | } 123 | .the-datepicker__main table.the-datepicker__calendar tr td, 124 | .the-datepicker__main table.the-datepicker__calendar tr th { 125 | display: table-cell; 126 | } 127 | .the-datepicker__main td.the-datepicker__cell { 128 | padding: 0; 129 | } 130 | .the-datepicker__main td.the-datepicker__cell a.the-datepicker__button { 131 | padding: 0.1em; 132 | } 133 | .the-datepicker__main td.the-datepicker__cell a.the-datepicker__button .the-datepicker__day-content { 134 | display: block; 135 | padding: 0.6em 0; 136 | } 137 | .the-datepicker__main td.the-datepicker__day a.the-datepicker__button .the-datepicker__day-content { 138 | text-align: center; 139 | border-radius: 0.3em; 140 | } 141 | .the-datepicker__main td.the-datepicker__day a.the-datepicker__button:hover { 142 | background-color: transparent; 143 | } 144 | .the-datepicker__main td.the-datepicker__day a.the-datepicker__button:hover .the-datepicker__day-content { 145 | background-color: #d1e8ff; 146 | } 147 | .the-datepicker__main td.the-datepicker__day--highlighted a.the-datepicker__button .the-datepicker__day-content { 148 | background-color: #d1e8ff; 149 | } 150 | .the-datepicker__main td.the-datepicker__day--outside a.the-datepicker__button { 151 | color: #8ac4ff; 152 | } 153 | .the-datepicker__main td.the-datepicker__day--unavailable a.the-datepicker__button { 154 | color: #aaa; 155 | } 156 | .the-datepicker__main td.the-datepicker__day--unavailable a.the-datepicker__button:hover .the-datepicker__day-content { 157 | background-color: transparent; 158 | } 159 | .the-datepicker__main td.the-datepicker__day--selected a.the-datepicker__button, 160 | .the-datepicker__main td.the-datepicker__day--selected.the-datepicker__day--highlighted a.the-datepicker__button { 161 | color: #fff; 162 | } 163 | .the-datepicker__main td.the-datepicker__day--selected a.the-datepicker__button .the-datepicker__day-content, 164 | .the-datepicker__main td.the-datepicker__day--selected.the-datepicker__day--highlighted a.the-datepicker__button .the-datepicker__day-content { 165 | background-color: #007eff; 166 | } 167 | .the-datepicker__main td.the-datepicker__day--selected a.the-datepicker__button:hover .the-datepicker__day-content, 168 | .the-datepicker__main td.the-datepicker__day--selected.the-datepicker__day--highlighted a.the-datepicker__button:hover .the-datepicker__day-content { 169 | background-color: #007eff; 170 | } 171 | .the-datepicker__main .the-datepicker__day--weekend, 172 | .the-datepicker__main .the-datepicker__week-day--weekend, 173 | .the-datepicker__main td.the-datepicker__day--today { 174 | font-weight: bold; 175 | } 176 | .the-datepicker__main .the-datepicker__animated { 177 | animation-duration: 0.1s; 178 | animation-fill-mode: both; 179 | } 180 | .the-datepicker__deselect-button { 181 | text-decoration: none; 182 | color: #007eff; 183 | font-weight: bold; 184 | } 185 | .the-datepicker__deselect-button:hover { 186 | text-decoration: none; 187 | } 188 | .the-datepicker__fade-out-left { 189 | animation-name: the-datepicker-fade-out-left; 190 | } 191 | .the-datepicker__fade-out-right { 192 | animation-name: the-datepicker-fade-out-right; 193 | } 194 | .the-datepicker__fade-in-left { 195 | animation-name: the-datepicker-fade-in-left; 196 | } 197 | .the-datepicker__fade-in-right { 198 | animation-name: the-datepicker-fade-in-right; 199 | } 200 | @-moz-keyframes the-datepicker-fade-out-left { 201 | from { 202 | transform: translate3d(0, 0, 0); 203 | } 204 | to { 205 | transform: translate3d(-100%, 0, 0); 206 | } 207 | } 208 | @-webkit-keyframes the-datepicker-fade-out-left { 209 | from { 210 | transform: translate3d(0, 0, 0); 211 | } 212 | to { 213 | transform: translate3d(-100%, 0, 0); 214 | } 215 | } 216 | @-o-keyframes the-datepicker-fade-out-left { 217 | from { 218 | transform: translate3d(0, 0, 0); 219 | } 220 | to { 221 | transform: translate3d(-100%, 0, 0); 222 | } 223 | } 224 | @keyframes the-datepicker-fade-out-left { 225 | from { 226 | transform: translate3d(0, 0, 0); 227 | } 228 | to { 229 | transform: translate3d(-100%, 0, 0); 230 | } 231 | } 232 | @-moz-keyframes the-datepicker-fade-out-right { 233 | from { 234 | transform: translate3d(0, 0, 0); 235 | } 236 | to { 237 | transform: translate3d(100%, 0, 0); 238 | } 239 | } 240 | @-webkit-keyframes the-datepicker-fade-out-right { 241 | from { 242 | transform: translate3d(0, 0, 0); 243 | } 244 | to { 245 | transform: translate3d(100%, 0, 0); 246 | } 247 | } 248 | @-o-keyframes the-datepicker-fade-out-right { 249 | from { 250 | transform: translate3d(0, 0, 0); 251 | } 252 | to { 253 | transform: translate3d(100%, 0, 0); 254 | } 255 | } 256 | @keyframes the-datepicker-fade-out-right { 257 | from { 258 | transform: translate3d(0, 0, 0); 259 | } 260 | to { 261 | transform: translate3d(100%, 0, 0); 262 | } 263 | } 264 | @-moz-keyframes the-datepicker-fade-in-left { 265 | from { 266 | transform: translate3d(-100%, 0, 0); 267 | } 268 | to { 269 | transform: translate3d(0, 0, 0); 270 | } 271 | } 272 | @-webkit-keyframes the-datepicker-fade-in-left { 273 | from { 274 | transform: translate3d(-100%, 0, 0); 275 | } 276 | to { 277 | transform: translate3d(0, 0, 0); 278 | } 279 | } 280 | @-o-keyframes the-datepicker-fade-in-left { 281 | from { 282 | transform: translate3d(-100%, 0, 0); 283 | } 284 | to { 285 | transform: translate3d(0, 0, 0); 286 | } 287 | } 288 | @keyframes the-datepicker-fade-in-left { 289 | from { 290 | transform: translate3d(-100%, 0, 0); 291 | } 292 | to { 293 | transform: translate3d(0, 0, 0); 294 | } 295 | } 296 | @-moz-keyframes the-datepicker-fade-in-right { 297 | from { 298 | transform: translate3d(100%, 0, 0); 299 | } 300 | to { 301 | transform: translate3d(0, 0, 0); 302 | } 303 | } 304 | @-webkit-keyframes the-datepicker-fade-in-right { 305 | from { 306 | transform: translate3d(100%, 0, 0); 307 | } 308 | to { 309 | transform: translate3d(0, 0, 0); 310 | } 311 | } 312 | @-o-keyframes the-datepicker-fade-in-right { 313 | from { 314 | transform: translate3d(100%, 0, 0); 315 | } 316 | to { 317 | transform: translate3d(0, 0, 0); 318 | } 319 | } 320 | @keyframes the-datepicker-fade-in-right { 321 | from { 322 | transform: translate3d(100%, 0, 0); 323 | } 324 | to { 325 | transform: translate3d(0, 0, 0); 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /routes/tickets.js: -------------------------------------------------------------------------------- 1 | const express = require("express"), 2 | router = express.Router(); 3 | moment = require("moment"); 4 | 5 | // Schemas 6 | const User = require ("../models/user"); 7 | Ticket = require ("../models/ticket"); 8 | Job = require ("../models/job"); 9 | Client = require ("../models/client"); 10 | 11 | // Functions 12 | let isLoggedIn = require("../functions/isLoggedIn"); 13 | let isAdministrator = require("../functions/isAdministrator"); 14 | let isManager = require("../functions/isManager"); 15 | 16 | router.use(function(req, res, next){ 17 | res.locals.currentUser = req.user; 18 | next(); 19 | }); 20 | 21 | require('dotenv').config(); 22 | 23 | const nodemailer = require('nodemailer'); 24 | 25 | // 1. Transporter 26 | let transporter = nodemailer.createTransport({ 27 | service: 'gmail', 28 | auth: { 29 | user: process.env.EMAIL, 30 | pass: process.env.PASSWORD 31 | } 32 | }); 33 | 34 | // ========================================================================== index 35 | router.get("/", isLoggedIn, async function(req, res){ 36 | let tickets = []; 37 | try { 38 | tickets = await Ticket.find({}).populate("created_by").populate("assigned_user").populate("completed_by_user").exec(); 39 | } 40 | catch (err) {console.log(err);} 41 | res.render("tickets/tickets", {tickets: tickets}); 42 | }); 43 | 44 | // ========================================================================== new 45 | router.get("/add", isLoggedIn, async function(req, res){ 46 | let users = []; 47 | try { 48 | users = await User.find({}); 49 | } 50 | catch (err) {console.log(err);} 51 | res.render("tickets/add_ticket", {users: users}); 52 | }) 53 | 54 | // ========================================================================== show 55 | router.get("/:id", isLoggedIn, async function(req, res){ 56 | let ticket = {}; 57 | try { 58 | ticket = await Ticket.findById(req.params.id).populate("created_by").populate("assigned_user").populate("completed_by_user").exec(); 59 | } 60 | catch (err) {console.log(err);} 61 | res.render("tickets/ticket", {ticket: ticket}); 62 | }); 63 | 64 | // ========================================================================== edit 65 | router.get("/:id/edit", isLoggedIn, isManager, async function(req, res){ 66 | let ticket = {}; 67 | let users = []; 68 | let due_date; 69 | try { 70 | ticket = await Ticket.findById(req.params.id).populate("created_by").populate("assigned_user").populate("completed_by_user").exec(); 71 | users = await User.find({}); 72 | due_date = moment(ticket.due_date).format("YYYY-MM-DD"); 73 | } 74 | catch (err) {console.log(err);} 75 | res.render("tickets/edit_ticket", {ticket: ticket, users: users, due_date: due_date}); 76 | }); 77 | 78 | // ========================================================================== create 79 | router.post("/", isLoggedIn, async function(req, res){ 80 | try { 81 | let ticket = req.body.ticket; 82 | ticket.due_date = moment(ticket.due_date); 83 | ticket.date_added = moment(moment(Date.now()).format("YYYY-MM-DD")); 84 | await Ticket.create(ticket); 85 | console.log("New Ticket Added"); 86 | } 87 | catch (err) {console.log(err);} 88 | 89 | // Email Ticket 90 | try { 91 | let created_by_user = await User.findById(req.body.ticket["created_by"]); 92 | let assigned_user = await User.findById(req.body.ticket["assigned_user"]); 93 | let cc_arr = []; 94 | if (assigned_user.email_address != created_by_user.email_address) { 95 | cc_arr.push (created_by_user.email_address) 96 | } 97 | 98 | // 2. 99 | let mailOptions = { 100 | from: 'zernst.crm.app@gmail.com', 101 | to: assigned_user.email_address, 102 | cc: cc_arr, 103 | subject: 'New Ticket ' + '"' + req.body.ticket["ticket_name"] + '"' + ' is due ' + moment(req.body.ticket["due_date"]).format("MM/DD/YYYY"), 104 | html: "

New Ticket

" + 105 | "

From: " + created_by_user.username + "

" + 106 | "

Assigned to: " + assigned_user.username + "

" + 107 | "

Due Date: " + moment(req.body.ticket["due_date"]).format("MM/DD/YYYY") + "

" + 108 | "

" + req.body.ticket["description"] + "

" 109 | } 110 | 111 | // 3. 112 | transporter.sendMail(mailOptions, function(err, data) { 113 | if (err){ 114 | console.log(err); 115 | } else { 116 | console.log("Ticket Email Sent Successfully"); 117 | } 118 | }); 119 | } 120 | catch (err) {console.log(err);} 121 | req.flash("success", "New ticket has been created"); 122 | res.redirect("/tickets"); 123 | }); 124 | 125 | // ========================================================================== update 126 | router.put("/:id", isLoggedIn, isManager, async function(req, res){ 127 | try { 128 | let ticket = req.body.ticket; 129 | ticket.due_date = moment(ticket.due_date); 130 | ticket.date_added = moment(moment(Date.now()).format("YYYY-MM-DD")); 131 | await Ticket.findByIdAndUpdate(req.params.id, ticket); 132 | console.log("Ticket " + "'" + ticket.ticket_name + "'" + " has been updated"); 133 | } 134 | catch (err) {console.log(err);} 135 | 136 | // Email Ticket 137 | try { 138 | let created_by_user = await User.findById(req.body.ticket["created_by"]); 139 | let assigned_user = await User.findById(req.body.ticket["assigned_user"]); 140 | let cc_arr = []; 141 | if (assigned_user.email_address != created_by_user.email_address) { 142 | cc_arr.push (assigned_user.email_address) 143 | } 144 | 145 | // 2. 146 | let mailOptions = { 147 | from: 'zernst.crm.app@gmail.com', 148 | to: assigned_user.email_address, 149 | cc: cc_arr, 150 | subject: 'Updated Ticket ' + '"' + req.body.ticket["ticket_name"] + '"' + ' is due ' + moment(req.body.ticket["due_date"]).format("MM/DD/YYYY"), 151 | html: "

This Ticket Has Been Updated

" + 152 | "

From: " + created_by_user.username + "

" + 153 | "

Assigned to: " + assigned_user.username + "

" + 154 | "

Due Date: " + moment(req.body.ticket["due_date"]).format("MM/DD/YYYY") + "

" + 155 | "

" + req.body.ticket["description"] + "

" 156 | } 157 | 158 | // 3. 159 | transporter.sendMail(mailOptions, function(err, data) { 160 | if (err){ 161 | console.log(err); 162 | } else { 163 | console.log("Ticket Email Sent Successfully"); 164 | } 165 | }); 166 | } 167 | catch (err) {console.log(err);} 168 | req.flash("info", "Ticket has been updated"); 169 | res.redirect("/tickets/" + req.params.id); 170 | }); 171 | 172 | // ========================================================================== delete 173 | router.delete("/:id", isLoggedIn, isAdministrator, async function(req, res){ 174 | // Delete Ticket 175 | try { 176 | await Ticket.findByIdAndRemove(req.params.id); 177 | console.log("Ticket has been deleted"); 178 | } 179 | catch (err) {console.log(err);} 180 | req.flash("error", "Ticket has been deleted"); 181 | res.redirect("/tickets"); 182 | }); 183 | 184 | // ========================================================================== mark ticket completed 185 | // ========================================================================== edit 186 | router.get("/:id/complete_form", isLoggedIn, async function(req, res){ 187 | let ticket = {}; 188 | let users = []; 189 | try { 190 | ticket = await Ticket.findById(req.params.id).populate("created_by").populate("assigned_user").populate("completed_by_user").exec(); 191 | users = await User.find({}); 192 | } 193 | catch (err) {console.log(err);} 194 | res.render("tickets/complete_ticket", {ticket: ticket, users: users}); 195 | }); 196 | 197 | // ========================================================================== update 198 | router.put("/complete/:id", isLoggedIn, async function(req, res){ 199 | // Update Ticket 200 | try { 201 | let ticket = req.body.ticket; 202 | ticket.completed_date = moment(ticket.completed_date); 203 | completed_ticket = await Ticket.findByIdAndUpdate(req.params.id, req.body.ticket); 204 | console.log("Ticket " + "'" + completed_ticket.ticket_name + "'" + " has been completed"); 205 | } 206 | catch (err) {console.log(err);} 207 | 208 | // Email Ticket 209 | try { 210 | let foundTicket = await Ticket.findById(req.params.id).populate("created_by").populate("assigned_user").populate("completed_by_user").exec(); 211 | let created_by_user = await User.findById(foundTicket["created_by"]); 212 | let assigned_user = await User.findById(foundTicket["assigned_user"]); 213 | let completed_by_user = await User.findById(req.body.ticket["completed_by_user"]); 214 | let cc_arr = []; 215 | 216 | if (assigned_user.email_address != completed_by_user.email_address) { 217 | cc_arr.push(assigned_user.email_address); 218 | } 219 | 220 | if (created_by_user.email_address != completed_by_user.email_address && created_by_user != assigned_user) { 221 | cc_arr.push(created_by_user.email_address); 222 | } 223 | 224 | // 2. 225 | let mailOptions = { 226 | from: 'zernst.crm.app@gmail.com', 227 | to: completed_by_user.email_address, 228 | cc: cc_arr, 229 | subject: 'Completed Ticket ' + '"' + foundTicket["ticket_name"] + '"' + ' was completed on ' + moment(req.body.ticket["completed_date"]).format("MM/DD/YYYY"), 230 | html: "

This Ticket Has Been Completed

" + 231 | "

From: " + created_by_user.username + "

" + 232 | "

Assigned to: " + assigned_user.username + "

" + 233 | "

Completed by: " + completed_by_user.username + "

" + 234 | "

Completed Date: " + moment(req.body.ticket["completed_date"]).format("MM/DD/YYYY") + "

" + 235 | "

" + req.body.ticket["completed_description"] + "

" 236 | } 237 | 238 | // 3. 239 | transporter.sendMail(mailOptions, function(err, data) { 240 | if (err){ 241 | console.log(err); 242 | } else { 243 | console.log("Ticket Email Sent Successfully"); 244 | } 245 | }); 246 | } 247 | catch (err) {console.log(err);} 248 | req.flash("success", "Ticket has been completed"); 249 | res.redirect("/tickets/" + req.params.id); 250 | }); 251 | 252 | module.exports = router; --------------------------------------------------------------------------------