Selaa lähdekoodia

Merge branch 'development'

Lee Morgan 6 vuotta sitten
vanhempi
sitoutus
9e250b1555
45 muutettua tiedostoa jossa 2435 lisäystä ja 485 poistoa
  1. 0 1
      app.js
  2. 27 14
      controllers/merchantData.js
  3. 38 16
      controllers/otherData.js
  4. 8 2
      controllers/recipeData.js
  5. 60 14
      controllers/renderer.js
  6. 6 7
      controllers/transactionData.js
  7. 2 2
      models/order.js
  8. 0 19
      package-lock.json
  9. 1 2
      package.json
  10. 23 20
      routes.js
  11. 12 0
      views/dashboardPage/components/addIngredient.ejs
  12. 42 0
      views/dashboardPage/components/addRecipe.ejs
  13. 344 0
      views/dashboardPage/components/components.css
  14. 22 0
      views/dashboardPage/components/ingredientDetails.ejs
  15. 75 0
      views/dashboardPage/components/menu.ejs
  16. 29 0
      views/dashboardPage/components/recipeDetails.ejs
  17. 283 0
      views/dashboardPage/controller.js
  18. 229 147
      views/dashboardPage/dashboard.css
  19. 69 214
      views/dashboardPage/dashboard.ejs
  20. 245 0
      views/dashboardPage/home.js
  21. 103 0
      views/dashboardPage/ingredients.js
  22. 5 0
      views/dashboardPage/orders.js
  23. 142 0
      views/dashboardPage/recipeBook.js
  24. 0 1
      views/dataPage/purchase.js
  25. 1 1
      views/informationPage/information.ejs
  26. 0 1
      views/landingPage/landing.ejs
  27. 0 0
      views/oldDashboardPage/account.js
  28. 0 0
      views/oldDashboardPage/addIngredient.js
  29. 215 0
      views/oldDashboardPage/dashboard.css
  30. 256 0
      views/oldDashboardPage/dashboard.ejs
  31. 0 0
      views/oldDashboardPage/enterPurchase.js
  32. 0 0
      views/oldDashboardPage/enterTransactions.js
  33. 0 0
      views/oldDashboardPage/inventory.js
  34. 0 0
      views/oldDashboardPage/recipes.js
  35. 0 0
      views/oldDashboardPage/singleRecipe.js
  36. 10 0
      views/passResetPage/passReset.css
  37. 56 0
      views/passResetPage/passReset.ejs
  38. 80 19
      views/shared/graphs.js
  39. BIN
      views/shared/images/downArrow.png
  40. BIN
      views/shared/images/homeIcon.png
  41. BIN
      views/shared/images/profile.png
  42. BIN
      views/shared/images/upArrow.png
  43. 0 0
      views/shared/oldController.js
  44. 10 5
      views/shared/shared.css
  45. 42 0
      views/shared/validation.js

+ 0 - 1
app.js

@@ -14,7 +14,6 @@ app.use(session({
     saveUninitialized: true,
     resave: false
 }));
-app.use(require("sanitize").middleware);
 app.use(express.static(__dirname + "/views"));
 app.use(express.urlencoded({extended: true}));
 app.use(express.json());

+ 27 - 14
controllers/merchantData.js

@@ -307,8 +307,9 @@ module.exports = {
 
     //POST - Update the quantity for a merchant inventory item
     //Inputs:
-    //  req.body.ingredientId: Id of ingredient to update
-    //  req.body.quantityChange: Amount to change ingredient (not the new value)
+    //  req.body.ingredients: array of ingredient data
+    //      id: id of ingredient to update
+    //      quantity: New value for the ingredient
     //Returns: Nothing
     updateMerchantIngredient: function(req, res){
         if(!req.session.user){
@@ -316,13 +317,34 @@ module.exports = {
             return res.redirect("/");
         }
 
+        let adjustments = [];
+
         Merchant.findOne({_id: req.session.user})
             .then((merchant)=>{
-                let updateIngredient = merchant.inventory.find(i => i.ingredient.toString() === req.body.ingredientId);
-                updateIngredient.quantity = (updateIngredient.quantity + req.body.quantityChange).toFixed(2);
+                for(let i = 0; i < req.body.length; i++){
+                    for(let j = 0; j < merchant.inventory.length; j++){
+                        if(merchant.inventory[j].ingredient.toString() === req.body[i].id){
+                            updateIngredient = merchant.inventory[j];
+                            break;
+                        }
+                    }
+
+                    adjustments.push(new InventoryAdjustment({
+                        date: Date.now(),
+                        merchant: req.session.user,
+                        ingredient: req.body[i].id,
+                        quantity: req.body[i].quantity - updateIngredient.quantity
+                    }));
+
+                    updateIngredient.quantity = req.body[i].quantity;
+                }
+
                 merchant.save()
                     .then((newMerchant)=>{
                         res.json({});
+
+                        InventoryAdjustment.create(adjustments).catch(()=>{});
+                        return;
                     })
                     .catch((err)=>{
                         return res.json("Error: your data could not be saved");
@@ -330,16 +352,7 @@ module.exports = {
             })
             .catch((err)=>{
                 return res.json("Error: your data could not be retrieved");
-            });
-
-        let invAdj = new InventoryAdjustment({
-            date: Date.now(),
-            merchant: req.session.user,
-            ingredient: req.body.ingredientId,
-            quantity: req.body.quantityChange
-        });
-
-        invAdj.save().catch((err)=>{});
+            });        
     },
 
     //POST - Adds an ingredient to a recipe

+ 38 - 16
controllers/otherData.js

@@ -2,14 +2,14 @@ const bcrypt = require("bcryptjs");
 const axios = require("axios");
 
 const Merchant = require("../models/merchant");
-const Purchase = require("../models/purchase");
+const Order = require("../models/order");
 const Transaction = require("../models/transaction");
 
 module.exports = {
-    //POST - Creates a new purchase for a merchant
+    //POST - Creates a new order for a merchant
     //Inputs:
-    //  req.body: list of purchases (ingredient id and quantity)
-    createPurchase: function(req, res){
+    //  req.body: list of orders (ingredient id and quantity)
+    createOrder: function(req, res){
         if(!req.session.user){
             req.session.error = "Must be logged in to do that";
             return res.redirect("/");
@@ -17,9 +17,9 @@ module.exports = {
 
         Merchant.findOne({_id: req.session.user})
             .then((merchant)=>{
-                for(let purchase of req.body){
-                    let merchantIngredient = merchant.inventory.find(i => i.ingredient._id.toString() === purchase.ingredient);
-                    merchantIngredient.quantity += Number(purchase.quantity);
+                for(let order of req.body){
+                    let merchantIngredient = merchant.inventory.find(i => i.ingredient._id.toString() === order.ingredient);
+                    merchantIngredient.quantity += Number(order.quantity);
                 }
                 
                 merchant.save()
@@ -34,12 +34,12 @@ module.exports = {
                 return res.json("Error: Unable to retrieve user data");
             });
 
-            let purchase = new Purchase({
+            let order = new Order({
                 merchant: req.session.user,
                 date: Date.now(),
                 ingredients: req.body
             });
-            purchase.save().catch((err)=>{});
+            order.save().catch((err)=>{});
     },
 
     //POST - logs the user in
@@ -132,13 +132,13 @@ module.exports = {
             });
     },
 
-    //POST - Gets transactions and purchases between 2 dates for a merchant
+    //POST - Gets transactions and orders between 2 dates for a merchant
     //Inputs:
     //  req.body.from = start date
     //  req.body.to = end date
     //Returns:
     //  transactions = list of transactions between the dates provided
-    //  purchases = list of purchases between the dates provided
+    //  orders = list of orders between the dates provided
     getData: function(req, res){
         if(!req.session.user){
             req.session.error = "Must be logged in to do that";
@@ -159,11 +159,11 @@ module.exports = {
             }));
 
             promiseList.push(new Promise((resolve, reject)=>{
-                Purchase.find({merchant: req.session.user, date: {$gte: req.body.dates[i], $lt: req.body.dates[i+1]}},
+                Order.find({merchant: req.session.user, date: {$gte: req.body.dates[i], $lt: req.body.dates[i+1]}},
                     {date: 1, ingredients: 1, _id: 0},
                     {sort: {date: 1}})
-                    .then((purchases)=>{
-                        resolve(purchases);
+                    .then((orders)=>{
+                        resolve(orders);
                     })
                     .catch((err)=>{})
             }));
@@ -176,7 +176,7 @@ module.exports = {
                 for(let i = 0; i < response.length; i+=2){
                     newList.push({
                         transactions: response[i],
-                        purchases: response[i+1]
+                        orders: response[i+1]
                     });
                 }
 
@@ -185,5 +185,27 @@ module.exports = {
             .catch((err)=>{
                 return res.json("Error: unable to retrieve user data");
             });
+    },
+
+    resetPassword: function(req, res){
+        Merchant.findOne({password: req.body.hash})
+            .then((merchant)=>{
+                if(merchant){
+                    let salt = bcrypt.genSaltSync(10);
+                    let hash = bcrypt.hashSync(req.body.pass, salt);
+
+                    merchant.password = hash;
+
+                    return merchant.save();
+                }else{
+                    req.session.error = "Error: unable to retrieve merchant data";
+                    return res.redirect("/");
+                }
+            })
+            .then((merchant)=>{
+                req.session.error = "Password successfully reset.  Please log in";
+                return res.redirect("/");
+            })
+            .catch((err)=>{});
     }
-}
+} 

+ 8 - 2
controllers/recipeData.js

@@ -5,6 +5,10 @@ module.exports = {
     //POST - creates a single new recipe
     //Inputs:
     //  req.body.name: name of recipes
+    //  req.body.price: price of the recipe
+    //  req.body.ingredients: array of ingredients (object) in recipe
+    //      id: id of ingredient
+    //      quantity: quantity of ingredient in recipe
     //Returns the newly created recipe
     createRecipe: function(req, res){
         if(!req.session.user){
@@ -16,9 +20,10 @@ module.exports = {
             merchant: req.session.user,
             name: req.body.name,
             price: Math.round(req.body.price * 100),
-            ingredients: []
+            ingredients: req.body.ingredients
         });
 
+
         Merchant.findOne({_id: req.session.user})
             .then((merchant)=>{
                 merchant.recipes.push(recipe);
@@ -33,9 +38,10 @@ module.exports = {
 
         recipe.save()
             .then((newRecipe)=>{
-                return res.json(newRecipe);
+                return res.json({});
             })
             .catch((err)=>{
+                console.log(err);
                 return res.json("Error: unable to save new ingredient");
             });
     }

+ 60 - 14
controllers/renderer.js

@@ -1,9 +1,9 @@
 const axios = require("axios");
+const ObjectId = require("mongoose").Types.ObjectId;
 
 const Merchant = require("../models/merchant");
-const Ingredient = require("../models/ingredient");
 const Transaction = require("../models/transaction");
-const Purchase = require("../models/purchase");
+const Order = require("../models/order");
 
 module.exports = {
     //GET - Shows the public landing page
@@ -96,26 +96,68 @@ module.exports = {
                             merchant.save()
                                 .then((updatedMerchant)=>{
                                     updatedMerchant.accessToken = undefined;
-                                    res.render("dashboardPage/dashboard", {merchant: updatedMerchant, error: undefined});
+                                    merchant = updatedMerchant;
+                                    
                                     Transaction.create(transactions);
-                                    return;
+
+                                    let date = new Date();
+                                    let firstDay = new Date(date.getFullYear(), date.getMonth() - 1, 1);
+
+                                    return Transaction.aggregate([
+                                        {$match: {
+                                            merchant: new ObjectId(req.session.user),
+                                            date: {$gte: firstDay}
+                                        }},
+                                        {$sort: {date: 1}},
+                                        {$project: {
+                                            date: 1,
+                                            recipes: 1
+                                        }}
+                                    ])
+                                })
+                                .then((transactions)=>{
+                                    res.render("dashboardPage/dashboard", {merchant: merchant, transactions: transactions});
                                 })
                                 .catch((err)=>{
-                                    let errorMessage = "Error: unable to save user data";
+                                    let errorMessage = "Error: unable to update data";
                                     
                                     merchant.password = undefined;
-                                    return res.render("dashboardPage/dashboard", {merchant: updatedMerchant, error: errorMessage});
+                                    return res.render("dashboardPage/dashboard", {merchant: merchant, error: errorMessage, transactions: []});
                                 });
                         })
                         .catch((err)=>{
+                            console.log("Fucking bitch error");
+                            console.log(err);
                             let errorMessage = "There was an error and we could not retrieve your transactions from Clover";
 
                             merchant.password = undefined;
-                            return res.render("dashboardPage/dashboard", {merchant: merchant, error: errorMessage});
+                            return res.render("dashboardPage/dashboard", {merchant: merchant, error: errorMessage, transactions: []});
                         });
                 }else if(merchant.pos === "none"){
                     merchant.password = undefined;
-                    return res.render("dashboardPage/dashboard", {merchant: merchant, error: undefined});
+
+                    let date = new Date();
+                    let firstDay = new Date(date.getFullYear(), date.getMonth() - 1, 1);
+
+                    Transaction.aggregate([
+                        {$match: {
+                            merchant: new ObjectId(req.session.user),
+                            date: {$gte: firstDay},
+                        }},
+                        {$sort: {date: 1}},
+                        {$project: {
+                            _id: 0,
+                            date: 1,
+                            recipes: 1
+                        }}
+                    ])
+                        .then((transactions)=>{
+                            return res.render("dashboardPage/dashboard", {merchant: merchant, transactions: transactions})
+                        })
+                        .catch((err)=>{
+                            console.log(err);
+                        });
+                        
                 }else{
                     req.session.error = "Error: WEBSITE PANIC";
                     
@@ -163,22 +205,22 @@ module.exports = {
                 .catch((err)=>{});
         });
 
-        let purchasePromise = new Promise((resolve, reject)=>{
-            Purchase.find({merchant: req.session.user, date: {$gte: firstDay, $lt: lastDay}},
+        let orderPromise = new Promise((resolve, reject)=>{
+            Order.find({merchant: req.session.user, date: {$gte: firstDay, $lt: lastDay}},
                 {date: 1, ingredients: 1, _id: 0},
                 {sort: {date: 1}})
-                .then((purchases)=>{
-                    resolve(purchases);
+                .then((orders)=>{
+                    resolve(orders);
                 })
                 .catch((err)=>{});
         });
 
-        Promise.all([merchTransPromise, purchasePromise])
+        Promise.all([merchTransPromise, orderPromise])
             .then((response)=>{
                 let data = {
                     merchant: response[0].merchant,
                     transactions: response[0].transactions,
-                    purchases: response[1],
+                    orders: response[1],
                     dates: [firstDay, lastDay]
                 }
 
@@ -189,5 +231,9 @@ module.exports = {
 
                 return res.redirect("/");
             });
+    },
+
+    displayPassReset: function(req, res){
+        return res.render("passResetPage/passReset");
     }
 }

+ 6 - 7
controllers/transactionData.js

@@ -1,5 +1,5 @@
 const Transaction = require("../models/transaction");
-const Purchase = require("../models/purchase");
+const Order = require("../models/order");
 const Merchant = require("../models/merchant");
 
 module.exports = {
@@ -37,18 +37,18 @@ module.exports = {
             });
     },
 
-    getPurchases: function(req, res){
+    getOrders: function(req, res){
         if(!req.session.user){
             req.session.error = "You must be logged in to view that page";
             return res.redirect("/");
         }
 
-        Purchase.find({merchant: req.session.user})
-            .then((purchases)=>{
-                return res.json(purchases);
+        Order.find({merchant: req.session.user})
+            .then((orders)=>{
+                return res.json(orders);
             })
             .catch((err)=>{
-                return res.json("Error: could not retrieve purchases data");
+                return res.json("Error: could not retrieve order data");
             })
     },
 
@@ -116,7 +116,6 @@ module.exports = {
         function randomDate() {
             let now = new Date();
             let start = new Date();
-            // start.setDate(now.getDate() - 10);
             start.setFullYear(now.getFullYear() - 1);
             return new Date(start.getTime() + Math.random() * (now.getTime() - start.getTime()));
         }

+ 2 - 2
models/purchase.js → models/order.js

@@ -1,6 +1,6 @@
 const mongoose = require("mongoose");
 
-const PurchaseSchema = new mongoose.Schema({
+const OrderSchema = new mongoose.Schema({
     merchant: {
         type: mongoose.Schema.Types.ObjectId,
         ref: "Merchant",
@@ -25,4 +25,4 @@ const PurchaseSchema = new mongoose.Schema({
     }]
 });
 
-module.exports = mongoose.model("Purchase", PurchaseSchema);
+module.exports = mongoose.model("Order", OrderSchema);

+ 0 - 19
package-lock.json

@@ -283,11 +283,6 @@
         "tsscmp": "1.0.6"
       }
     },
-    "lodash": {
-      "version": "4.17.15",
-      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
-      "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
-    },
     "media-typer": {
       "version": "0.3.0",
       "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -480,15 +475,6 @@
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
       "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
     },
-    "sanitize": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/sanitize/-/sanitize-2.1.0.tgz",
-      "integrity": "sha512-HLDVriFJnrm6ElDe2E8alAKDMZGMtM8CdKhvunp9592j8hNwZmmsmhk/t6WZbWonKJsHK0OoxH5S1Yoie4sSpw==",
-      "requires": {
-        "lodash": "^4.17.0",
-        "validator": "^3.33.0"
-      }
-    },
     "semver": {
       "version": "5.7.1",
       "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
@@ -581,11 +567,6 @@
       "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
       "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
     },
-    "validator": {
-      "version": "3.43.0",
-      "resolved": "https://registry.npmjs.org/validator/-/validator-3.43.0.tgz",
-      "integrity": "sha1-lkZLmS1BloM9l6GUv0Cxn/VLrgU="
-    },
     "vary": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

+ 1 - 2
package.json

@@ -23,7 +23,6 @@
     "cookie-session": "^1.4.0",
     "ejs": "^2.7.1",
     "express": "^4.17.1",
-    "mongoose": "^5.7.4",
-    "sanitize": "^2.1.0"
+    "mongoose": "^5.7.4"
   }
 }

+ 23 - 20
routes.js

@@ -10,41 +10,44 @@ module.exports = function(app){
     app.get("/", renderer.landingPage);
     app.get("/dashboard", renderer.displayDashboard);
     app.get("/information", renderer.displayLegal);
-    app.get("/data", renderer.displayData);
+    app.get("/resetpassword/*", renderer.displayPassReset);
 
     //Merchant
     app.post("/merchant/create/none", merchantData.createMerchantNone);
-    app.get("/merchant/create/clover", merchantData.createMerchantClover);
-    app.get("/merchant/recipes/update", merchantData.updateRecipes);
-    app.post("/merchant/recipes/remove", merchantData.removeRecipe);
-    app.post("/merchant/ingredients/create", merchantData.addMerchantIngredient);
-    app.post("/merchant/ingredients/remove", merchantData.removeMerchantIngredient);
-    app.post("/merchant/ingredients/update", merchantData.updateMerchantIngredient);
-    app.post("/merchant/recipes/ingredients/create", merchantData.addRecipeIngredient);
-    app.post("/merchant/recipes/ingredients/update", merchantData.updateRecipeIngredient);
-    app.post("/merchant/recipes/ingredients/remove", merchantData.removeRecipeIngredient);
-    app.post("/merchant/update", merchantData.updateMerchant);
-    app.post("/merchant/password", merchantData.updatePassword);
+    app.post("/merchant/create/clover", merchantData.createMerchantClover);
+    // app.get("/merchant/recipes/update", merchantData.updateRecipes);
+    // app.post("/merchant/recipes/remove", merchantData.removeRecipe);
+    // app.post("/merchant/ingredients/add/:id", merchantData.addMerchantIngredient);
+    // app.post("/merchant/ingredients/remove", merchantData.removeMerchantIngredient);
+    app.put("/merchant/ingredients/update", merchantData.updateMerchantIngredient);
+    // app.post("/merchant/recipes/ingredients/create", merchantData.addRecipeIngredient);
+    // app.post("/merchant/recipes/ingredients/update", merchantData.updateRecipeIngredient);
+    // app.post("/merchant/recipes/ingredients/remove", merchantData.removeRecipeIngredient);
+    // app.post("/merchant/update", merchantData.updateMerchant);
+    // app.post("/merchant/password", merchantData.updatePassword);
 
     //Ingredients
-    app.get("/ingredients", ingredientData.getIngredients);
-    app.post("/ingredients/createone", ingredientData.createIngredient);  //also adds to merchant
+    // app.get("/ingredients", ingredientData.getIngredients);
+    // app.post("/ingredients/create", ingredientData.createIngredient);  //also adds to merchant
 
     //Recipes
-    app.post("/recipe/create", recipeData.createRecipe);
+    // app.post("/recipe/create", recipeData.createRecipe);
+
+    //Orders
 
     //Other
-    app.post("/purchases/create", otherData.createPurchase);
+    // app.post("/purchases/create", otherData.createOrder);
     app.post("/login", otherData.login);
     app.get("/logout", otherData.logout);
     app.get("/cloverlogin", otherData.cloverRedirect);
     app.get("/cloverauth*", otherData.cloverAuth);
+    app.post("/resetpassword", otherData.resetPassword);
 
-    app.post("/getdata", otherData.getData);
+    // app.post("/getdata", otherData.getData);
 
     //Transactions
-    app.post("/transactions", transactionData.getTransactions);
-    app.get("/purchases", transactionData.getPurchases);
-    app.post("/transactions/create", transactionData.createTransaction);  //Creates transaction for non-pos merchant
+    // app.post("/transactions", transactionData.getTransactions);
+    app.get("/orders", transactionData.getOrders);
+    // app.post("/transactions/create", transactionData.createTransaction);  //Creates transaction for non-pos merchant
     app.get("/populatesometransactions", transactionData.populate);
 }

+ 12 - 0
views/dashboardPage/components/addIngredient.ejs

@@ -0,0 +1,12 @@
+<div id="addIngredient">
+    <button class="sidebarIconButton" onclick="closeSidebar()">
+        <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <line x1="5" y1="12" x2="19" y2="12"></line>
+            <polyline points="12 5 19 12 12 19"></polyline>
+        </svg>
+    </button>
+    
+    <h1>Add New Ingredients</h1>
+
+    <input type="text">
+</div>

+ 42 - 0
views/dashboardPage/components/addRecipe.ejs

@@ -0,0 +1,42 @@
+<div id="addRecipe">
+    <button class="sidebarIconButton" onclick="closeSidebar()">
+        <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <line x1="5" y1="12" x2="19" y2="12"></line>
+            <polyline points="12 5 19 12 12 19"></polyline>
+        </svg>
+    </button>
+
+    <h1>New Recipe</h1>
+
+    <div class="recipeBasicInfo">
+        <label>Name:
+            <input id="newRecipeName" type="text">
+        </label>
+
+        <label>Price:
+            <input id="newRecipePrice" type="number" step="0.01" min="0">
+        </label>
+
+        <label># of Ingredients
+            <input id="ingredientCount" type="number" step="1" min="1" onchange="recipeBookStrandObj.changeRecipeCount()">
+        </label>
+    </div>
+
+    <h2>Ingredients</h2>
+    
+    <div id="recipeInputIngredients">
+        <div>
+            <h4>Ingredient 1</h4>
+
+            <label>Ingredient:
+                <select></select>
+            </label>
+
+            <label>Quantity:
+                <input type="number" step="0.01" min="0">
+            </label>
+        </div>
+    </div>
+    
+    <button class="button" onclick="recipeBookStrandObj.submitNewRecipe()">Create</button>
+</div>

+ 344 - 0
views/dashboardPage/components/components.css

@@ -0,0 +1,344 @@
+/* Menu */
+.menu{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    background: rgb(0, 27, 45);
+    width: 18vw;
+    height: 100vh;
+    transition: width 0.3s;
+    flex-grow: 0;
+    flex-shrink: 0;
+}
+
+    .menuHead{
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        width: 100%;
+        padding: 0 20px;
+        margin-top: 10px;
+        font-size: 22px;
+        font-weight: bold;
+        margin-bottom: 100px;
+        text-decoration: none;
+    }
+
+    .menuLogo{
+        width: 25%;
+        height: 25%;
+    }
+
+    .menuHead a{
+        display: flex;
+        align-items: center;
+        color: white;
+        text-decoration: none;
+        width: 80%;
+    }
+
+    .menuHead p{
+        margin-left: 25px;
+    }
+
+    .menuHead button{
+        background: none;
+        border-style: none;
+        color: white;
+        font-size: 35px;
+        cursor: pointer;
+        outline: none;
+    }
+
+    .menuHead button:hover{
+        color: rgb(201, 201, 201);
+    }
+
+    .menuHead > button::-moz-focus-inner{
+        border: 0;
+    }
+
+    .menu > button svg{
+        stroke: white;
+        margin-right: 25px;
+    }
+
+    .menu > button{
+        display: flex;
+        align-items: center;
+        background: none;
+        border-style: none;
+        color: white;
+        font-size: 20px;
+        margin: 15px 0;
+        padding-left: 50px;
+        width: 100%;
+        height: 75px;
+        cursor: pointer;
+        text-align: left;
+        border: 0;
+    }
+
+    .menu > button::-moz-focus-inner{
+        border: 0;
+    }  
+
+    .menu > button:hover{
+        background: rgb(201, 201, 201);
+        color: black;
+        fill: black;
+    }
+
+    .menu > button:hover svg{
+        stroke: black;
+    }
+
+.menu > .active{
+    background: rgb(179, 191, 209);
+    cursor: default;
+}
+
+.menu > .active:hover{
+    background: rgb(179, 191, 209);
+    color: white;
+}
+
+.menu > .active:hover svg{
+    stroke: white;
+}
+
+.logout{
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    position: absolute;
+    bottom: 25px;
+    color: white;
+    text-decoration: none;
+    font-size: 20px;
+    border-radius: 5px;
+    padding: 10px;
+}
+
+.logout img{
+    height: 25px;
+    width: 25px;
+}
+
+.logout > *{
+    margin: 10px;
+}
+
+.logout:hover{
+    background: rgb(179, 191, 209);
+    color: black;
+}
+
+/* Minimized menu */
+.menuMinimized{
+    width: 5vw;
+}
+
+.menuLogoMin{
+    width: 4vw;
+}
+
+.menuHeadMin{
+    display: none;
+    flex-direction: column;
+}
+
+.menuMinimized button{
+    justify-content: center;
+    padding: 0;
+}
+
+.menuMinimized button svg{
+    margin: 0;
+}
+
+.sidebar{
+    display: flex;
+    width: 25vw;
+    height: 100vh;
+    box-sizing: border-box;
+    padding: 25px;
+    transition: width 0.2s;
+    overflow: hidden;
+    box-shadow: -1px 0px 10px gray;
+}
+
+.sidebarHide{
+    width: 0;
+    transition: width 0.2s;
+    margin: 0;
+    padding: 0;
+    overflow: hidden;
+    box-shadow: -1px 0px 10px gray;
+}
+
+    .sidebarHide > *{
+        width: 0;
+        transition: width 0.2s;
+        visibility: hidden;
+    }
+
+.sidebar button:first-of-type{
+    background: none;
+    border: none;
+    cursor: pointer;
+    padding: 3px;
+    border-radius: 5px;
+    margin-right: auto;
+}
+
+    .sidebar button:first-of-type:hover{
+        background: rgb(0, 27, 45);
+        color: white;
+    }
+
+.lineBorder{
+    width: 100%;
+    border-bottom: 1px solid gray;
+    margin: 25px;
+}
+
+#addIngredient{
+    flex-direction: column;
+    width: 100%;
+}
+
+#ingredientDetails{
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+}
+
+    #ingredientDetails > p{
+        border-radius: 10px;
+        background: gray;
+        color: white;
+        padding: 0 3px;
+        font-size: 14px;
+    }
+
+    #ingredientDetails label{
+        font-size: 20px;
+        text-align: center;
+    }
+
+    #ingredientDetails label p{
+        font-size: 30px;
+        font-weight: bold;
+        padding: 10px;
+    }
+
+#recipeDetails{
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+}
+
+    #recipeDetailsButtons{
+        display: flex;
+        justify-content: space-between;
+        width: 100%;
+        margin-bottom: 100px;
+    }
+
+    #recipeDetailsButtons button{
+        background: none;
+        border: none;
+        cursor: pointer;
+        margin: 0;
+        padding: 3px;
+        border-radius: 5px;
+    }
+
+        #recipeDetailsButtons button:hover{
+            background: rgb(0, 27, 45);
+            color: white;
+        }
+
+    #recipeIngredients{
+        width: 100%;
+    }
+
+    .recipeIngredient{
+        display: flex;
+        justify-content: space-between;
+        margin: 25px;
+    }
+
+    #recipePrice{
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+    }
+
+    #recipePrice p{
+        font-size: 25px;
+        font-weight: bold;
+    }
+
+#addRecipe{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    width: 100%;
+    padding-bottom: 50px;
+}
+
+    #addRecipe label{
+        display: flex;
+        justify-content: space-between;
+        margin: 15px 0;
+    }
+
+    #addRecipe input{
+        width: 50%;
+    }
+
+    #addRecipe h2{
+        margin-top: 10px;
+    }
+
+    .recipeBasicInfo{
+        width: 75%;
+        background: rgb(240, 252, 255);
+        padding: 15px;
+        border-radius: 5px;
+        border: 1px solid black;
+    }
+
+    #recipeInputIngredients{
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        box-sizing: border-box;
+        margin: 0 0 25px 0;
+        width:75%;
+        overflow-y: auto;
+        overflow-x: hidden;
+        box-shadow: 0 0 2px gray;
+        border-radius: 5px;
+    }
+
+    #recipeInputIngredients div{
+        width: 75%;
+        background: rgb(240, 252, 255);
+        border: 1px solid black;
+        padding: 10px;
+        border-radius: 5px;
+        margin: 10px 0;
+        padding: 10px 15px;
+        text-align: center;
+    }
+
+    #recipeInputIngredients div h4{
+        color: rgb(255, 99, 107);
+    }
+
+    #addRecipe button:last-of-type{
+        margin-top: auto;
+    }

+ 22 - 0
views/dashboardPage/components/ingredientDetails.ejs

@@ -0,0 +1,22 @@
+<div id="ingredientDetails">
+    <button class="sidebarIconButton" onclick="closeSidebar()">
+        <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <line x1="5" y1="12" x2="19" y2="12"></line>
+            <polyline points="12 5 19 12 12 19"></polyline>
+        </svg>
+    </button>
+
+    <p></p>
+    <h1></h1>
+    <div class="lineBorder"></div>
+
+    <label>Current Stock
+        <p id="ingredientStock"></p>
+    </label>
+
+    <div class="lineBorder"></div>
+
+    <label>Average Daily Use (30 days)
+        <p id="dailyUse"></p>
+    </label>
+</div>

+ 75 - 0
views/dashboardPage/components/menu.ejs

@@ -0,0 +1,75 @@
+<div class="menu">
+    <div id="max" class="menuHead">
+        <a href="/">
+            <img class="menuLogo" src="/shared/images/logo.png" alt="The Subline">
+            <p>The Subline</p>
+        </a>
+
+        <button onclick="changeMenu()">&#8801;</button>
+    </div>
+
+    <div id="min" class="menuHead menuHeadMin">
+        <a href="/">
+            <img class="menuLogoMin" src="/shared/images/logo.png" alt="The Subline">
+        </a>
+
+        <button onclick="changeMenu()">&#8801;</button>
+    </div>
+
+    <button id="homeBtn" class="active" onclick="changeStrand('homeStrand')">
+        <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>
+        <p>Home</p>
+    </button>
+
+    <button id="ingredientsBtn" onclick="changeStrand('ingredientsStrand')">
+        <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-archive"><polyline points="21 8 21 21 3 21 3 8"></polyline><rect x="1" y="3" width="22" height="5"></rect><line x1="10" y1="12" x2="14" y2="12"></line></svg>
+        <p>Ingredients</p>
+    </button>
+
+    <button id="recipeBookBtn" onclick="changeStrand('recipeBookStrand')">
+        <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-book"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path></svg>
+        <p>Recipe Book</p>
+    </button>
+
+    <button id="ordersBtn" onclick="changeStrand('ordersStrand')">
+        <svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart"><circle cx="9" cy="21" r="1"></circle><circle cx="20" cy="21" r="1"></circle><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path></svg>
+        <p>Orders</p>
+    </button>
+
+    <a class="logout" href="/logout">
+        <p>Logout</p>
+        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
+            <polyline points="16 17 21 12 16 7"></polyline>
+            <line x1="21" y1="12" x2="9" y2="12"></line>
+        </svg>
+    </a>
+
+    <script>
+        let changeMenu = ()=>{
+            let menu = document.querySelector(".menu");
+            let buttons = document.querySelectorAll(".menu > button");
+            if(!menu.classList.contains("menuMinimized")){
+                menu.classList = "menu menuMinimized";
+
+                for(let button of buttons){
+                    button.children[1].style.display = "none";
+                }
+
+                document.querySelector("#max").style.display = "none";
+                document.querySelector("#min").style.display = "flex";
+            }else if(menu.classList.contains("menuMinimized")){
+                menu.classList = "menu";
+
+                for(let button of buttons){
+                    button.children[1].style.display = "block";
+                }
+
+                setTimeout(()=>{
+                    document.querySelector("#max").style.display = "flex";
+                    document.querySelector("#min").style.display = "none";
+                }, 150);
+            }
+        }
+    </script>
+</div>

+ 29 - 0
views/dashboardPage/components/recipeDetails.ejs

@@ -0,0 +1,29 @@
+<div id="recipeDetails">
+    <div id="recipeDetailsButtons">
+        <button class="sidebarIconButton" onclick="closeSidebar()">
+            <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <line x1="5" y1="12" x2="19" y2="12"></line>
+                <polyline points="12 5 19 12 12 19"></polyline>
+            </svg>
+        </button>
+
+        <button class="sidebarIconButton" onclick="recipeBookStrandObj.editRecipe()">
+            <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                <path d="M12 20h9"></path>
+                <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
+            </svg>
+        </button>
+    </div>
+
+    <h1></h1>
+
+    <div id="recipeIngredients"></div>
+
+    <div class="lineBorder"></div>
+
+    <div id="recipePrice">
+        <h3>Price</h3>
+        
+        <p></p>
+    </div>
+</div>

+ 283 - 0
views/dashboardPage/controller.js

@@ -0,0 +1,283 @@
+/* 
+Changes to a different strand
+Input:
+ name: name of the strand.  Must end with "Strand"
+*/
+let changeStrand = (name)=>{
+    closeSidebar();
+
+    for(let strand of document.querySelectorAll(".strand")){
+        strand.style.display = "none";
+    }
+
+    for(let button of document.querySelectorAll(".menu > button")){
+        button.classList = "";
+        button.onclick = ()=>{changeStrand(`${button.id.slice(0, button.id.indexOf("Btn"))}Strand`)};
+    }
+
+    let activeButton = document.querySelector(`#${name.slice(0, name.indexOf("Strand"))}Btn`);
+    activeButton.classList = "active";
+    activeButton.onclick = undefined;
+
+    document.querySelector(`#${name}`).style.display = "flex";
+    window[`${name}Obj`].display();
+}
+
+/*
+Updates all specified item in the merchant's inventory and updates the page
+If ingredient doesn't exist, add it
+Inputs:
+ Array of objects
+     id: id of ingredient
+     quantity: updated quantity (optional)
+     name: name of ingredient (only for new ingredient)
+     category: category of ingredient (only for new ingredient)
+     unit: unit of measurement (only for new ingredient)
+ remove: if true, remove ingredient from inventory
+ */
+let updateInventory = (ingredients, remove = false)=>{
+    for(let i = 0; i < ingredients.length; i++){
+        let isNew = true;
+        for(let j = 0; j < merchant.inventory.length; j++){
+            if(merchant.inventory[j].ingredient._id === ingredients[i].id){
+                if(remove){
+                    merchant.inventory.splice(i, 1);
+                }else{
+                    merchant.inventory[j].quantity = ingredients[i].quantity;
+                }
+
+                isNew = false;
+                break;
+            }
+        }
+
+        if(isNew){
+            merchant.inventory.push(ingredients[i]);
+        }
+    }
+
+    homeStrandObj.drawInventoryCheckCard();
+    ingredientsStrandObj.populateIngredients();
+}
+
+//Close any open sidebar
+let closeSidebar = ()=>{
+    let sidebar = document.querySelector("#sidebarDiv");
+    for(let i = 0; i < sidebar.children.length; i++){
+        sidebar.children[i].style.display = "none";
+    }
+    sidebar.classList = "sidebarHide";
+}
+
+/*
+Open a specific sidebar
+Input:
+ sidebar: the outermost element of the sidebar (must contain class sidebar)
+*/
+let openSidebar = (sidebar)=>{
+    document.querySelector("#sidebarDiv").classList = "sidebar";
+
+    let sideBars = document.querySelector("#sidebarDiv").children;
+    for(let i = 0; i < sideBars.length; i++){
+        sideBars[i].style.display = "none";
+    }
+
+    sidebar.style.display = "flex";
+}
+
+/*
+Gets the indices of two dates from transactions
+Inputs
+ from: starting date
+ to: ending date (default to now)
+Output
+ Array containing starting index and ending index
+Note: Will return false if it cannot find both necessary dates
+*/
+let dateIndices = (from, to = new Date())=>{
+    let indices = [];
+
+    for(let i = 0; i < transactions.length; i++){
+        if(transactions[i].date > from){
+            indices.push(i);
+            break;
+        }
+    }
+
+    for(let i = transactions.length - 1; i >=0; i--){
+        if(transactions[i].date < to){
+            indices.push(i);
+            break;
+        }
+    }
+
+    if(indices.length < 2){
+        return false;
+    }
+
+    return indices;
+}
+/*
+Gets the quantity of each ingredient sold between two dates (dateRange)
+Inputs
+ dateRange: list containing a start date and an end date
+Output
+ List of objects
+     id: id of specific ingredient
+     quantity: quantity sold of that ingredient
+     name: name of the ingredient
+*/
+let ingredientsSold = (dateRange)=>{
+    if(!dateRange){
+        return false;
+    }
+    
+    let recipes = recipesSold(dateRange);
+    let ingredientList = [];
+
+    for(let i = 0; i < recipes.length; i++){
+        for(let j = 0; j < merchant.recipes.length; j++){
+            for(let k = 0; k < merchant.recipes[j].ingredients.length; k++){
+                let exists = false;
+                for(let l = 0; l < ingredientList.length; l++){
+                    if(ingredientList[l].id === merchant.recipes[j].ingredients[k].ingredient._id){
+                        exists = true;
+                        ingredientList[l].quantity += merchant.recipes[j].ingredients[k].quantity * recipes[i].quantity;
+                        break;
+                    }
+                }
+
+                if(!exists){
+                    ingredientList.push({
+                        id: merchant.recipes[j].ingredients[k].ingredient._id,
+                        quantity: merchant.recipes[j].ingredients[k].quantity * recipes[i].quantity,
+                        name: merchant.recipes[j].ingredients[k].ingredient.name,
+                        unit: merchant.recipes[j].ingredients[k].ingredient.unit
+                    })
+                }
+            }
+        }
+    }
+
+    return ingredientList;
+}
+/*
+Gets the quantity of a single ingredient sold between two dates (dateRange)
+Input:
+    dateRange: array containing two elements, start and end indices
+    id: id of the ingredient to calculate
+Return: (int) Quantity of recipes sold
+*/
+let ingredientSold = (dateRange,  id)=>{
+    let recipes = recipesSold(dateRange);
+    let total = 0;
+
+    let checkRecipes = [];
+    let quantities = [];
+    for(let i = 0; i < merchant.recipes.length; i++){
+        for(let j = 0; j < merchant.recipes[i].ingredients.length; j++){
+            if(merchant.recipes[i].ingredients[j].ingredient._id === id){
+                checkRecipes.push(merchant.recipes[i]._id);
+                quantities.push(merchant.recipes[i].ingredients[j].quantity);
+                break;
+            }
+        }
+    }
+
+    for(let i = 0; i < recipes.length; i++){
+        for(let i = 0; i < checkRecipes.length; i++){
+            if(checkRecipes[i] === recipes[i].id){
+                total += recipes[i].quantity * quantities[i];
+                break;
+            }
+        }
+    }
+
+    return total;
+}
+/*
+Gets the number of recipes sold between two dates (dateRange)
+Inputs:
+    dateRange: array containing a start date and an end date
+Return:
+    List of objects
+        id: id of specific recipe
+        quantity: quantity sold of that recipe
+*/
+let recipesSold = (dateRange)=>{
+    let recipeList = [];
+
+    for(let i = dateRange[0]; i <= dateRange[1]; i++){
+        for(let j = 0; j < transactions[i].recipes.length; j++){
+            let exists = false;
+            for(let k = 0; k < recipeList.length; k++){
+                if(recipeList[k].id === transactions[i].recipes[j].recipe){
+                    exists = true;
+                    recipeList[k].quantity += transactions[i].recipes[j].quantity;
+                    break;
+                }
+            }
+
+            if(!exists){
+                recipeList.push({
+                    id: transactions[i].recipes[j].recipe,
+                    quantity: transactions[i].recipes[j].quantity
+                })
+            }
+        }
+    }
+
+    return recipeList;
+}
+
+/*
+Groups all of the merchant's ingredients by their category
+Return:
+    Array of objects
+        ingredients: Array of objects
+            id: Id of ingredient
+            name: Name of ingredient
+            quantity:  Merchant's quantity of this ingredient
+            unit: Measurement unit
+        name: Category name
+*/
+let categorizeIngredients = ()=>{
+    let ingredientsByCategory = [];
+
+    for(let i = 0; i < merchant.inventory.length; i++){
+        let categoryExists = false;
+        for(let j = 0; j < ingredientsByCategory.length; j++){
+            if(merchant.inventory[i].ingredient.category === ingredientsByCategory[j].name){
+                ingredientsByCategory[j].ingredients.push({
+                    id: merchant.inventory[i].ingredient._id,
+                    name: merchant.inventory[i].ingredient.name,
+                    quantity: merchant.inventory[i].quantity,
+                    unit: merchant.inventory[i].ingredient.unit
+                });
+
+                categoryExists = true;
+                break;
+            }
+        }
+
+        if(!categoryExists){
+            ingredientsByCategory.push({
+                name: merchant.inventory[i].ingredient.category,
+                ingredients: [{
+                    id: merchant.inventory[i].ingredient._id,
+                    name: merchant.inventory[i].ingredient.name,
+                    quantity: merchant.inventory[i].quantity,
+                    unit: merchant.inventory[i].ingredient.unit
+                }]
+            });
+        }
+    }
+
+    return ingredientsByCategory;
+}
+
+for(let transaction of transactions){
+    transaction.date = new Date(transaction.date);
+}
+
+homeStrandObj.display();

+ 229 - 147
views/dashboardPage/dashboard.css

@@ -1,215 +1,297 @@
-#title{
-    font-size: 45px;
-    color: rgb(255, 99, 107);
+html{
+    height: 100%;
+    max-width: 100%;
+}
+
+body{
+    display: flex;
+    flex-direction: row;
+    height: 100%;
+    max-width: 100%;
+}
+
+.contentBlock{
+    width: 100%;
+    height: 100vh;
+    flex: 1;
+    box-sizing: border-box;
+    padding: 50px;
+    padding-top: 0;
+}
+
+.strand{
+    flex-direction: column;
+    height: 100%;
+    width: 100%;
+}
+
+.strandTitle{
+    text-align: left;
+    margin: 15px;
+    padding: 0;
+}
+
+/* Cards */
+.card{
+    margin: 0 15px;
+    border: 1px solid rgba(183, 183, 183, 0.9);
+    border-radius: 7px;
+    box-shadow: 1px 2px gray;
+    flex-grow: 1;
+    height: 100%;
+}
+
+.card > *{
+    margin: 10px;
+}
+
+.card p:first-of-type{
+    font-size: 14px;
+    font-weight: bold;
     text-align: center;
-    margin-top: 25px;
 }
 
-.cursor{
-    cursor: pointer;
+/* Home Strand */
+#homeStrand{
+    display: flex;
+}
+
+.flexRow{
+    display: flex;
+    justify-content: space-around;
+    width: 100%;
+    flex-basis: 100px;
+    flex-grow: 1;
+    margin: 15px 0;
 }
 
-/* Inventory Strand */
-#inventoryStrand{
+#revenueCard{
+    display: flex;
     flex-direction: column;
+    justify-content: center;
     align-items: center;
 }
 
-    #inventoryStrand > *{
-        margin: 10px;
+    #revenue{
+        text-align: center;
+        font-size: 30px;
+        font-weight: bold;
     }
 
-    #inventoryStrand tbody{
-        text-transform: capitalize;
+    #revenueChange{
+        display: flex;
     }
 
-    #filter{
-        max-height: 32px;
+    #revenueChange > p{
+        font-weight: 100;
     }
 
-/* Recipes Strand */
-#recipesStrand{
-    flex-direction: column;
-    align-items: center;
+    #revenueChange img{
+        height: 15px;
+        width: 15px;
+        margin-right: 10px;
+    }
+
+#graphCard{
+    flex-grow: 5;
+    display: flex;
+    justify-content: center;
 }
 
-    #recipesStrand > *{
-        margin: 15px;
-    }
+#inventoryCheckCard{
+    display: flex;
+    flex-direction: column;;
+    justify-content: space-between;
+    flex-grow: 0.1;
+}
 
-    #recipeUpdate{
-        margin-bottom: 10px;
+    #inventoryCheckCard ul{
+        list-style: none;
     }
 
-    #newRecipe{
-        display: none;
-        justify-content: center;
+    #inventoryCheckCard li{
+        margin: 5px 0;
     }
 
-    #recipesContainer{
-        display: flex;
-        justify-content: space-around;
-        flex-wrap: wrap;
-        padding: 25px;
-    }
-
-        .recipe-card{
-            border: 2px solid rgb(255, 99, 107);
-            box-shadow: 2px 2px 2px rgb(0, 27, 45);
-            background: rgb(0, 27, 45);
-            color: rgb(255, 99, 107);
-            width: 20%;
-            min-width: 200px;
-            padding: 35px;
-            margin: 25px;
-            border-radius: 10px;
-            cursor: pointer;
-        }
-
-            .recipe-card:hover{
-                transform: translateY(-3px);
-            }
-
-            .empty-recipe{
-                background: rgb(255, 99, 107);
-                border: 2px solid rgb(0, 27, 45);
-                color: rgb(0, 27, 45);
-            }
-
-/* Account Strand */
-#accountStrand{
-    flex-direction: column;
-    align-items: center;
-}
+    #inventoryCheckCard li > p{
+        font-weight: 300;
+        font-size: 15px;
+        margin-right: 25px;
+    }
 
-    #accountStrand > *{
-        margin: 15px;
+    #inventoryCheckCard li > p:nth-of-type(1){
+        width: 50%;
     }
 
-    #accountStrand label{
-        display: flex;
+    #inventoryCheckCard li input{
+        width: 75px;
+        margin-right: 25px;
+        font-size: 20px;
+        text-align: center;
     }
 
-    #accountEdit{
-        display: none;
+    #inventoryCheckCard li p:nth-of-type(2){
+        width: 25%;
+        min-width: 75px;
     }
 
-    #passwordEdit{
-        display: none;
+    #inventoryCheckCard button{
+        width: 150px;
+        text-align: center;
+        margin-left: auto;
     }
 
-/* Add Ingredient Action */
-#addIngredientAction{
+#popularIngredientsCard{
+    display: flex;
     flex-direction: column;
     align-items: center;
 }
 
-    #addIngredientAction > *{
-        margin: 25px;
+    .notice{
+        color: red;
+        font-size: 35px;
+        font-weight: bold;
     }
 
-    .container{
-        display: flex;
-        justify-content: space-around;
-        align-items: center;
-    }
-
-        .container > *{
-            margin: 50px;
-        }
-
-        .container > div{
-            display: flex;
-            flex-direction: column;
-            align-items: center;
-        }
+/* Ingredients Strand */
+#ingredientsStrand{
+    display: none;
+    align-items: center;
+    height: 100%;
+    box-sizing: border-box;
+}
 
-            .container > div > *{
-                margin: 10px;
-            }
+#ingredientsStrand > h1{
+    margin-bottom: 100px;
+}
 
-/* Enter Transactions Action */
-#enterTransactionsAction{
-    flex-direction: column;
+#ingredientHead{
+    display: flex;
+    justify-content: space-between;
     align-items: center;
+    margin-bottom: 50px;
+    width: 100%;
 }
-    #enterTransactionsAction > *{
-        margin: 10px;
-    }
 
-/* Enter Purchase Action */
-#enterPurchaseAction{
-    flex-direction: column;
+#ingredientHead button{
+    display: flex;
     align-items: center;
 }
 
-    #enterPurchaseAction > *{
-        margin: 10px;
-    }
+#ingredientHead svg{
+    margin-right: 5px;
+}
 
-/* Single Recipe Action */
-#singleRecipeAction{
-    display: none;
-    flex-direction: column;
+#categoryList{
+    align-items:center;
+    overflow-y: auto;
+    width: 80%;
+}
+
+.categoryDiv > div:first-of-type{
+    display: flex;
     margin: auto;
+    width: 95%;
+}
+
+.categoryDiv > div:first-of-type p{
+    display: flex;
     align-items: center;
+    padding: 20px 0 20px 100px;
+    background: rgb(240, 252, 255);
+    border: 1px solid gray;
+    border-radius: 5px;
+    margin-right: 25px;
+    flex-grow: 1;
 }
 
-    #singleRecipeAction > *{
-        margin: 15px;
-    }
+.categoryDiv > div:first-of-type button{
+    padding: 10px;
+    background: rgb(240, 252, 255);
+    border: 1px solid black;
+    border-radius: 5px;
+    width: 75px;
+    height: 75px;
+    cursor: pointer;
+}
 
-    #delRecipe{
-        display: "none"
-    }
+.ingredientsDiv{
+    flex-direction: column;
+}
 
-@media screen and (max-width: 1000px){
-    /* General use */
-    body{
-        text-align: center;
-    }
+.ingredient{
+    display: flex;
+    width: 50%;
+    padding: 15px;
+    margin: auto;
+    cursor: pointer;
+    border-radius: 5px;
+}
 
-    /* Single Recipe Action */
-    #singleRecipeAction .buttonsDiv{
-        font-size: 15px;
-    }
+.ingredient:hover{
+    background: rgb(0, 27, 45);
+    color: white;
+}
 
-    #singleRecipeAction input{
-        max-width: 100px;
-    }
+.ingredient > *{
+    width: 33%;
+    margin-right: 15px;
 }
 
-@media screen and (max-width: 600px){
-    /* Inventory Strand */
-    #inventoryStrand h1{
-        font-size: 20px;
-        text-align: center;
-        margin: 5px;
-    }
+.ingredient > p:last-of-type{
+    text-align: right;
+}
 
-    .options{
-        flex-direction: column;
-    }
+.ingredientSpacer{
+    border: none;
+    border-top: 1px dashed black;
+    margin: auto;
+}
 
-    .options > *{
-        margin: 5px;
-    }
+/* Recipe Book Strand */
+#recipeBookStrand{
+    display: none;
+    align-items: center;
+}
 
-    #inventoryStrand .button-small{
-        font-size: 12px;
+    #recipeHead{
+        display: flex;
+        justify-content: space-between;
+        width: 100%;
+        align-items: center;
     }
 
-    #inventoryStrand td, #inventoryStrand th{
-        padding: 3px;
-        font-size: 12px;
+    #recipeHead svg{
+        margin-right: 5px
     }
 
-    /* addIngredientStrand */
-    .container{
+    #recipeList{
+        display: flex;
         flex-direction: column;
+        align-items: center;
+        width: 50%;
+        margin-top: 50px;
+        overflow-y: auto;
     }
 
-    #addIngredientAction > *{
-        margin: 10px;
+    .recipeItem{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        background: rgb(240, 252, 255);
+        border-radius: 5px;
+        padding: 20px 20px;
+        width: 90%;
+        border: 1px solid gray;
+        cursor: pointer;
+    }
+
+    .recipeItem:hover{
+        background: rgb(0, 27, 45);
+        color: white;
     }
+
+/* Orders Strand */
+#ordersStrand{
+    display: none;
 }

+ 69 - 214
views/dashboardPage/dashboard.ejs

@@ -1,256 +1,111 @@
 <!DOCTYPE html>
-<html lang="en">
+<html>
     <head>
         <meta charset="UTF-8">
         <title>The Subline</title>
         <link rel="icon" type="img/png" href="/shared/images/logo.png">
-        <link rel="stylesheet" href="/dashboardPage/dashboard.css">
         <link rel="stylesheet" href="/shared/shared.css">
+        <link rel="stylesheet" href="/dashboardPage/dashboard.css">
+        <link rel="stylesheet" href="/dashboardPage/components/components.css">
+        <link href="https://fonts.googleapis.com/css?family=Saira&display=swap" rel="stylesheet"> 
     </head>
     <body>
-        <% include ../shared/header %>
-
-        <% include ../shared/banner %>
-
-        <h1 id="title"><%=merchant.name%></h1>
-
-        <strand-selector></strand-selector>
-
-        <div id="inventoryStrand" class="strand">
-            <div class="buttonBox">
-                <button class="button" onclick="addIngredientObj.display()">Add Ingredient</button>
-
-                <button class="button" onclick="enterPurchaseObj.display()">Enter Purchases</button>
-
-                <% if(merchant.pos === "none"){ %>
-                    <button class="button" onclick="enterTransactionsObj.display()">Enter Transactions</button>
-                <% } %>
-
-                <a class="button" href="/data">Analyze data</a>
-            </div>
-
-            <input id="filter" onkeyup="inventoryObj.filter()" type="text" placeholder="FILTER INGREDIENTS">
-
-            <table>
-                <thead>
-                    <tr>
-                        <th class="cursor" onclick="inventoryObj.sortIngredients('name')">Item</th>
-                        <th class="cursor" onclick="inventoryObj.sortIngredients('category')">Category</th>
-                        <th class="cursor" onclick="inventoryObj.sortIngredients('quantity')">Quantity</th>
-                        <th class="cursor" onclick="inventoryObj.sortIngredients('unit')">Unit</th>
-                        <th>Actions</th>
-                    </tr>
-                    <tbody></tbody>
-                </thead>
-            </table>
-        </div>
-
-        <div id="recipesStrand" class="strand">
-            <div>
-                <% if(merchant.pos === "none"){ %>
-                    <button class="button" onclick="window.recipesObj.showInput()">Add a Recipe</button>
-                <% }else{%>
-                    <button class="button" id="recipeUpdate" onclick="recipesObj.updateRecipes()">Update Recipes</button>
-                <% } %>
-            </div>
-
-            <div id="newRecipe">
-                <label>Name:
-                    <input id="newName" type="text">
-                </label>
-
-                <label>Price:
-                    <input id="newPrice" type="number" step="0.01">
-                </label>
-
-                <button class="button-small" onclick="window.recipesObj.submitNew()">Create</button>
+        <% include ./components/menu %>
 
-                <button class="button-small" onclick="window.recipesObj.cancelAdd()">Cancel</button>
-            </div>
+        <div class="contentBlock">
+            <% include ../shared/banner %>
             
-            <div id="recipesContainer"></div>
-        </div>
-
-        <div id="accountStrand" class="strand">
-            <form id="accountDisplay" onsubmit="accountObj.editAccount()">
-                <label>Name:&nbsp;&nbsp;
-                    <p><%=merchant.name%></p>
-                </label>
+            <div id="homeStrand" class="strand">
+                <h1 class="strandTitle">Dashboard</h1>
 
-                <label>Email:&nbsp;&nbsp;
-                    <p><%=merchant.email%></p>
-                </label>
+                <div class="flexRow">
+                    <div id="revenueCard" class="card">
+                        <p>Total Revenue (month)</p>
 
-                <button class="button" onclick="accountObj.editAccount()">Edit</button>
-            </form>
+                        <p id="revenue"></p>
 
-            <form id="accountEdit">
-                <label>Name:&nbsp;&nbsp;
-                    <input id="accountName" type="text" value="<%=merchant.name%>">
-                </label>
+                        <div id="revenueChange">
+                            <img alt="revenue">
 
-                <label>Email:&nbsp;&nbsp;
-                    <input id="accountEmail" type="email" value="<%=merchant.email%>">
-                </label>
+                            <p></p>
+                        </div>
+                    </div>
 
-                <div class="buttonBox">
-                    <button class="button" onclick="accountObj.updateAccount()">Save</button>
-
-                    <button class="button" onclick="accountObj.editAccountCancel()">Cancel</button>
+                    <div id="graphCard" class="card">
+                        <canvas id="graphCanvas">
+                    </div>
                 </div>
-            </form>
-
-            <form id="passwordEdit" onsubmit="accountObj.updatePassword()">
-                <label>Old password:
-                    <input id="oldPass" type="password" required>
-                </label>
 
-                <label>New password:
-                    <input id="newPass" type="password" required>
-                </label>
+                <div class="flexRow">
+                    <div id="inventoryCheckCard" class="card">
+                        <p>Inventory Check</p>
 
-                <label>Confirm new password:
-                    <input id="confirmNewPass" type="password" required>
-                </label>
+                        <ul></ul>
 
+                        <button class="button" onclick="homeStrandObj.submitInventoryCheck()">Update</button>
+                    </div>
 
-                <div class="buttonBox">
-                    <input class="button" type="submit" value="Submit">
+                    <div id=popularIngredientsCard class="card">
+                        <p>Most Popular Ingredients (month)</p>
 
-                    <button class="button" onclick="accountObj.editPasswordCancel()">Cancel</button>
+                        <canvas id="popularCanvas"></canvas>
+                    </div>
                 </div>
-            </form>
-
-            <button class="button" onclick="accountObj.editPassword()">Change password</button>
-        </div>
-
-
-        <div id="addIngredientAction" class="action">
-            <div class="container">
-                <div>
-                    <h2>Choose Ingredients</h2>
-
-                    <input id="addFilter" type="text" placeholder="FILTER INGREDIENTS" onkeyup="window.addIngredientObj.filterAndDisplay()">
+            </div>
 
-                    <table>
-                        <thead>
-                            <tr>
-                                <th>Add</th>
-                                <th class="cursor" onclick="window.addIngredientObj.sort(1)">Ingredient</th>
-                                <th class="cursor" onclick="window.addIngredientObj.sort(2)">Category</th>
-                                <th class="cursor" onclick="window.addIngredientObj.sort(3)">Unit</th>
-                                <th>Quantity</th>
-                            </tr>
-                        </thead>
-                        <tbody></tbody>
-                    </table>
+            <div id="ingredientsStrand" class="strand">
+                <div id="ingredientHead">
+                    <h1 class="strandTitle">Ingredient Inventory</h1>
 
-                    <button class="button" onclick="addIngredientObj.submitAdd()">Submit</button>
+                    <button class="button" onclick="ingredientsStrandObj.displayAddIngredient()">
+                        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
+                        add
+                    </button>
                 </div>
 
-                <h1>Or</h1>
-
-                <form id="createIngredientInput" onsubmit="addIngredientObj.submitNew()">
-                    <h2>Create New Ingredient</h2>
-
-                    <label>Name
-                        <input id="newIngName" type="text" required>
-                    </label>
-
-                    <label>Category
-                        <input id="newCategory" type="text" required>
-                    </label>
-
-                    <label>Unit
-                        <input id="newUnit" type="text" required>
-                    </label>
-
-                    <label>Quantity
-                        <input id="newQuantity" type="number" step="0.01" required>
-                    </label>
-
-                    <button class="button">Create Ingredient</button>
-                </form>
+                <div id="categoryList"></div>
             </div>
-        </div>
 
+            <div id="recipeBookStrand" class="strand">
+                <div id="recipeHead">
+                    <h1 class="strandTitle">Recipe Book</h1>
 
-        <div id="enterTransactionsAction" class="action">
-            <h1>Enter all sales</h1>
-            <h3>Last updated: <span id="updated"></span></h3>
-            
-            <table>
-                <thead>
-                    <tr>
-                        <th>Recipe</th>
-                        <th>Number sold</th>
-                    </tr>
-                </thead>
-                <tbody></tbody>
-            </table>
-
-            <button class="button" onclick="enterTransactionsObj.submit()">Submit</button>
-        </div>
-
+                    <% if(merchant.pos === "none"){ %>
+                        <button class="button" onclick="recipeBookStrandObj.displayAddRecipe()">
+                            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
+                            add
+                        </button>
+                    <% } %>
+                </div>
 
-        <div id="enterPurchaseAction" class="action">
-            <h1>Enter Purchases</h1>
-            
-            <table>
-                <thead>
-                    <tr>
-                        <th>Ingredient</th>
-                        <th>Amount</th>
-                    </tr>
-                </thead>
-                <tbody></tbody>
-            </table>
+                <div id="recipeList"></div>
+            </div>
 
-            <button class="button" onclick="enterPurchaseObj.submit()">Submit</button>
+            <div id="ordersStrand" class="strand">
+                <h1 class="strandTitle">Orders</h1>
+            </div>
         </div>
 
+        <div id="sidebarDiv" class="sidebarHide">
+            <% include ./components/addIngredient %>
 
-        <div id="singleRecipeAction" class="action">
-            <h2 id="recipeName"></h2>
+            <% include ./components/ingredientDetails %>
 
-            <div class="buttonBox">
-                <button class="button" id="addButton">Add Ingredient</button>
-                <% if(merchant.pos === "none"){ %>
-                    <button class="button" id="removeButton">Remove</button>
-                <% } %>
-            </div>
+            <% include ./components/recipeDetails %>
 
-            <table>
-                <thead>
-                    <tr>
-                        <th>Name</th>
-                        <th>Quantity</th>
-                        <th>Actions</th>
-                    </tr>
-                </thead>
-                <tbody></tbody>
-            </table>
+            <% include ./components/addRecipe %>
         </div>
 
-        <%- include ../shared/footer %>
-
-        <script>
-                <% if(locals.error){ %>
-                    let error = <%- JSON.stringify(error) %>;
-                <% }else{ %>
-                    let error = undefined;
-                <% } %>
-        </script>
         <script>let merchant = <%- JSON.stringify(merchant) %>;</script>
-        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+        <script>let transactions = <%- JSON.stringify(transactions) %>;</script>
+        <script src="../shared/graphs.js"></script>
+        <script src="/dashboardPage/home.js"></script>
+        <script src="/dashboardPage/ingredients.js"></script>
+        <script src="/dashboardPage/recipeBook.js"></script>
+        <script src="/dashboardPage/controller.js"></script>
+        <script src="/dashboardPage/orders.js"></script>
         <script src="../shared/validation.js"></script>
-        <script src="/dashboardPage/inventory.js"></script>
-        <script src="/dashboardPage/recipes.js"></script>
-        <script src="/dashboardPage/account.js"></script>
-        <script src="/dashboardPage/addIngredient.js"></script>
-        <script src="/dashboardPage/enterTransactions.js"></script>
-        <script src="/dashboardPage/enterPurchase.js"></script>
-        <script src="/dashboardPage/singleRecipe.js"></script>
-        <script src="/shared/controller.js"></script>
+
+        <noscript>Please turn on javascript for this site to work properly</noscript>
     </body>
 </html>

+ 245 - 0
views/dashboardPage/home.js

@@ -0,0 +1,245 @@
+window.homeStrandObj = {
+    isPopulated: false,
+    graph: {},
+
+    display: function(){
+        if(!this.isPopulated){
+            this.drawRevenueCard();
+            this.drawRevenueGraph();
+            this.drawInventoryCheckCard();
+            this.drawPopularCard();
+
+            this.isPopulated = true;
+        }
+    },
+
+    drawRevenueCard: function(){
+        let today = new Date();
+        let firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
+        let firstOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
+        let lastMonthtoDay = new Date(new Date().setMonth(today.getMonth() - 1));
+
+        let revenueThisMonth = this.calculateRevenue(dateIndices(firstOfMonth));
+        let revenueLastmonthToDay = this.calculateRevenue(dateIndices(firstOfLastMonth, lastMonthtoDay));
+
+        document.querySelector("#revenue").innerText = `$${revenueThisMonth.toLocaleString("en")}`;
+
+        let revenueChange = ((revenueThisMonth - revenueLastmonthToDay) / revenueLastmonthToDay) * 100;
+        
+        let img = "";
+        if(revenueChange >= 0){
+            img = "/shared/images/upArrow.png";
+        }else{
+            img = "/shared/images/downArrow.png";
+        }
+        document.querySelector("#revenueChange p").innerText = `${Math.abs(revenueChange).toFixed(2)}% vs last month`;
+        document.querySelector("#revenueChange img").src = img;
+    },
+
+    drawRevenueGraph: function(){
+        let graphCanvas = document.querySelector("#graphCanvas");
+        let today = new Date();
+
+        graphCanvas.height = graphCanvas.parentElement.clientHeight;
+        graphCanvas.width = graphCanvas.parentElement.clientWidth;
+
+        this.graph = new LineGraph(graphCanvas);
+        this.graph.addTitle("Revenue");
+
+        let thirtyAgo = new Date(today);
+        thirtyAgo.setDate(today.getDate() - 29);
+
+        let data = this.graphData(dateIndices(thirtyAgo));
+        if(data){
+            this.graph.addData(
+                data,
+                [thirtyAgo, new Date()],
+                "Revenue"
+            );
+        }else{
+            document.querySelector("#graphCanvas").style.display = "none";
+            
+            let notice = document.createElement("h1");
+            notice.innerText = "NO DATA YET";
+            notice.classList = "notice";
+            document.querySelector("#graphCard").appendChild(notice);
+        }
+    },
+
+    drawInventoryCheckCard: function(){
+        let num;
+        if(merchant.inventory.length < 5){
+            num = merchant.inventory.length;
+        }else{
+            num = 5;
+        }
+        let rands = [];
+            for(let i = 0; i < num; i++){
+                let rand = Math.floor(Math.random() * merchant.inventory.length);
+
+                if(rands.includes(rand)){
+                    i--;
+                }else{
+                    rands[i] = rand;
+                }
+            }
+
+            let ul = document.querySelector("#inventoryCheckCard ul");
+            while(ul.children.length > 0){
+                ul.removeChild(ul.firstChild);
+            }
+            for(let i = 0; i < rands.length; i++){
+                let li = document.createElement("li");
+                li.classList = "flexRow";
+                li.ingredientIndex = rands[i];
+                ul.appendChild(li);
+
+                let name = document.createElement("p");
+                name.innerText = merchant.inventory[rands[i]].ingredient.name;
+                li.appendChild(name);
+
+                let input = document.createElement("input");
+                input.type = "number";
+                input.value = merchant.inventory[rands[i]].quantity;
+                li.appendChild(input);
+
+                let label = document.createElement("p");
+                label.innerText = merchant.inventory[rands[i]].ingredient.unit;
+                li.appendChild(label);
+            }
+    },
+
+    drawPopularCard: function(){
+        let dataArray = [];
+        let now = new Date();
+        let thisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
+
+        let ingredientList = ingredientsSold(dateIndices(thisMonth));
+        if(ingredientList){
+            for(let i = 0; i < 5; i++){
+                let max = ingredientList[0].quantity
+                let index = 0;
+                for(let j = 0; j < ingredientList.length; j++){
+                    if(ingredientList[j].quantity > max){
+                        max = ingredientList[j].quantity;
+                        index = j;
+                    }
+                }
+
+                dataArray.push({
+                    num: max,
+                    label: `${ingredientList[index].name}: ${ingredientList[index].quantity} ${ingredientList[index].unit}`
+                });
+                ingredientList.splice(index, 1);
+            }
+
+            let thisCanvas = document.querySelector("#popularCanvas");
+            thisCanvas.width = thisCanvas.parentElement.clientWidth;
+            thisCanvas.height = thisCanvas.parentElement.clientHeight * 0.75;
+
+            let popularGraph = new HorizontalBarGraph(document.querySelector("#popularCanvas"));
+            popularGraph.addData(dataArray);
+        }else{
+            document.querySelector("#popularCanvas").style.display = "none";
+
+            let notice = document.createElement("p");
+            notice.innerText = "N/A";
+            notice.classList = "notice";
+            document.querySelector("#popularIngredientsCard").appendChild(notice);
+        }
+    },
+
+    calculateRevenue: function(indices){
+        let total = 0;
+
+        for(let i = indices[0]; i <= indices[1]; i++){
+            for(let j = 0; j < transactions[i].recipes.length; j++){
+                for(let k = 0; k < merchant.recipes.length; k++){
+                    if(transactions[i].recipes[j].recipe === merchant.recipes[k]._id){
+                        total += transactions[i].recipes[j].quantity * merchant.recipes[k].price;
+                    }
+                }
+            }
+        }
+
+        return total / 100;
+    },
+
+    /*
+    Create the data for the revenue graph
+    Input: 
+        dateRange: Array containing start and end indices for transactions
+    Return: List of revenue by day between the dates specified
+    */
+    graphData: function(dateRange){
+        if(!dateRange){
+            return false;
+        }
+
+        let dataList = new Array(30).fill(0);
+        let currentDate = transactions[dateRange[0]].date;
+        let arrayIndex = 0;
+
+        for(let i = dateRange[0]; i <= dateRange[1]; i++){
+            if(transactions[i].date.getDate() !== currentDate.getDate()){
+                currentDate = transactions[i].date;
+                arrayIndex++;
+            }
+            for(let j = 0; j < transactions[i].recipes.length; j++){
+                for(let merchRecipe of merchant.recipes){
+                    if(transactions[i].recipes[j].recipe === merchRecipe._id){
+                        dataList[arrayIndex] = parseFloat((dataList[arrayIndex] + (transactions[i].recipes[j].quantity * merchRecipe.price) / 100).toFixed(2));
+                        break;
+                    }
+                }
+            }
+        }
+
+        return dataList;
+    },
+
+    submitInventoryCheck: function(){
+        let lis = document.querySelectorAll("#inventoryCheckCard ul li");
+
+        let changes = [];
+
+        for(let i = 0; i < lis.length; i++){
+            if(lis[i].children[1].value >= 0){
+                let merchIngredient = merchant.inventory[lis[i].ingredientIndex];
+
+                let value = parseInt(lis[i].children[1].value);
+
+                if(value !== merchIngredient.quantity){
+                    changes.push({
+                        id: merchIngredient.ingredient._id,
+                        quantity: value
+                    });
+                }
+            }else{
+                banner.createError("Cannot have negative ingredients");
+                return;
+            }
+        }
+        
+        if(changes.length > 0){
+            fetch("/merchant/ingredients/update", {
+                method: "PUT",
+                headers: {
+                    "Content-Type": "application/json;charset=utf-8"
+                },
+                body: JSON.stringify(changes)
+            })
+                .then((response)=>{
+                    banner.createError("Ingredients updated");
+                    if(typeof(response.data) === "string"){
+                        console.log(err);
+                    }else{
+                        updateInventory(changes);
+                    }
+                })
+                .catch((err)=>{
+                    console.log(err);
+                });
+        }
+    }
+}

+ 103 - 0
views/dashboardPage/ingredients.js

@@ -0,0 +1,103 @@
+window.ingredientsStrandObj = {
+    isPopulated: false,
+
+    display: function(){
+        if(!this.isPopulated){
+            this.populateIngredients();
+
+            this.isPopulated = true;
+        }
+    },
+
+    populateIngredients: function(){
+        let categories = categorizeIngredients();
+
+        let ingredientStrand = document.querySelector("#categoryList");
+        while(ingredientStrand.children.length > 0){
+            ingredientStrand.removeChild(ingredientStrand.firstChild);
+        }
+        for(let category of categories){
+            let categoryDiv = document.createElement("div");
+            categoryDiv.classList = "categoryDiv"
+            ingredientStrand.appendChild(categoryDiv);
+
+            let headerDiv = document.createElement("div");
+            categoryDiv.appendChild(headerDiv);
+            
+            let headerTitle = document.createElement("p");
+            headerTitle.innerText = category.name;
+            headerDiv.appendChild(headerTitle);
+
+            let ingredientsDiv = document.createElement("div");
+            ingredientsDiv.classList = "ingredientsDiv";
+            ingredientsDiv.style.display = "none";
+            categoryDiv.appendChild(ingredientsDiv);
+
+            let headerButton = document.createElement("button");
+            headerButton.innerHTML = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
+            headerButton.onclick = ()=>{this.toggleCategory(ingredientsDiv, headerButton)};
+            headerDiv.appendChild(headerButton);
+
+            for(let ingredient of category.ingredients){
+                let ingredientDiv = document.createElement("div");
+                ingredientDiv.classList = "ingredient";
+                ingredientDiv.onclick = ()=>{this.displayIngredient(ingredient, category)};
+                ingredientsDiv.appendChild(ingredientDiv);
+
+                let ingredientName = document.createElement("p");
+                ingredientName.innerText = ingredient.name;
+                ingredientDiv.appendChild(ingredientName);
+
+                let spacer = document.createElement("hr");
+                spacer.classList = "ingredientSpacer";
+                ingredientDiv.appendChild(spacer);
+
+                let ingredientData = document.createElement("p");
+                ingredientData.innerText = `${ingredient.quantity} ${ingredient.unit}`;
+                ingredientDiv.appendChild(ingredientData);
+            }
+        }
+    },
+
+    //Open or close the list of ingredients for a category
+    toggleCategory: function(div, button){
+        if(div.style.display === "none"){
+            button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="18 15 12 9 6 15"></polyline></svg>';
+            div.style.display = "flex";
+        }else if(div.style.display === "flex"){
+            button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>';
+            div.style.display = "none";
+        }
+    },
+
+    displayAddIngredient: function(){
+        let sidebar = document.querySelector("#addIngredient");
+        openSidebar(sidebar);
+    },
+
+    displayIngredient: function(ingredient, category){
+        sidebar = document.querySelector("#ingredientDetails");
+        openSidebar(sidebar);
+
+        document.querySelector("#ingredientDetails p").innerText = category.name;
+        document.querySelector("#ingredientDetails h1").innerText = ingredient.name;
+        document.querySelector("#ingredientStock").innerText = `${ingredient.quantity} ${ingredient.unit}`;
+
+        let quantities = [];
+        let now = new Date();
+        for(let i = 1; i < 31; i++){
+            let endDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i)
+            let startDay = new Date(now.getFullYear(), now.getMonth(), now.getDate() - i - 1);
+            quantities.push(ingredientSold(dateIndices(startDay, endDay), ingredient.id));
+        }
+
+        let sum = 0;
+        for(let quantity of quantities){
+            sum += quantity;
+        }
+
+        document.querySelector("#dailyUse").innerText = `${(sum/quantities.length).toFixed(2)} ${ingredient.unit}`;
+    },
+
+    
+}

+ 5 - 0
views/dashboardPage/orders.js

@@ -0,0 +1,5 @@
+window.ordersStrandObj = {
+    display: function(){
+        console.log("displaying orders");
+    }
+}

+ 142 - 0
views/dashboardPage/recipeBook.js

@@ -0,0 +1,142 @@
+window.recipeBookStrandObj = {
+    isPopulated: false,
+
+    display: function(){
+        if(!this.isPopulated){
+            this.populateRecipes();
+
+            this.isPopulated = true;
+        }
+    },
+
+    populateRecipes: function(){
+        let recipeList = document.querySelector("#recipeList");
+
+        for(let recipe of merchant.recipes){
+            let recipeDiv = document.createElement("div");
+            recipeDiv.classList = "recipeItem";
+            recipeDiv.onclick = ()=>{this.displayRecipe(recipe)};
+            recipeList.appendChild(recipeDiv);
+
+            let recipeName = document.createElement("p");
+            recipeName.innerText = recipe.name;
+            recipeDiv.appendChild(recipeName);
+
+            let recipePrice = document.createElement("p");
+            recipePrice.innerText = `$${(recipe.price / 100).toFixed(2)}`;
+            recipeDiv.appendChild(recipePrice);
+        }
+    },
+
+    displayRecipe: function(recipe){
+        openSidebar(document.querySelector("#recipeDetails"));
+
+        document.querySelector("#recipeDetails h1").innerText = recipe.name;
+
+        let ingredientList = document.querySelector("#recipeIngredients");
+        while(ingredientList.children.length > 0){
+            ingredientList.removeChild(ingredientList.firstChild);
+        }
+
+        for(let ingredient of recipe.ingredients){
+            let ingredientDiv = document.createElement("div");
+            ingredientDiv.classList = "recipeIngredient";
+            ingredientList.appendChild(ingredientDiv);
+
+            let ingredientName = document.createElement("p");
+            ingredientName.innerText = ingredient.ingredient.name;
+            ingredientDiv.appendChild(ingredientName);
+
+            let ingredientQuantity = document.createElement("p");
+            ingredientQuantity.innerText = `${ingredient.quantity} ${ingredient.ingredient.unit}`;
+            ingredientDiv.appendChild(ingredientQuantity);
+        }
+
+        document.querySelector("#recipePrice p").innerText = `$${(recipe.price / 100).toFixed(2)}`;
+    },
+
+    displayAddRecipe: function(){
+        openSidebar(document.querySelector("#addRecipe"));
+
+        let ingredientsSelect = document.querySelector("#recipeInputIngredients select");
+        let categories = categorizeIngredients();
+        for(let category of categories){
+            let optgroup = document.createElement("optgroup");
+            optgroup.label = category.name;
+            ingredientsSelect.appendChild(optgroup);
+
+            for(let ingredient of category.ingredients){
+                let option = document.createElement("option");
+                option.value = ingredient.id;
+                option.innerText = ingredient.name;
+                optgroup.appendChild(option);
+            }
+        }
+    },
+
+    //Updates the number of ingredient inputs displayed for new recipes
+    changeRecipeCount: function(){
+        let newCount = document.querySelector("#ingredientCount").value;
+        let ingredientsDiv = document.querySelector("#recipeInputIngredients");
+        let oldCount = ingredientsDiv.children.length;
+
+        if(newCount > oldCount){
+            let newDivs = newCount - oldCount;
+
+            for(let i = 0; i < newDivs; i++){
+                let newNode = ingredientsDiv.children[0].cloneNode(true);
+                newNode.children[2].children[0].value = "";
+
+                ingredientsDiv.appendChild(newNode);
+            }
+
+            for(let i = 0; i < newCount; i++){
+                ingredientsDiv.children[i].children[0].innerText = `Ingredient ${i + 1}`;
+            }
+        }else if(newCount < oldCount){
+            let newDivs = oldCount - newCount;
+
+            for(let i = 0; i < newDivs; i++){
+                ingredientsDiv.removeChild(ingredientsDiv.children[ingredientsDiv.children.length-1]);
+            }
+        }
+    },
+
+    submitNewRecipe: function(){
+        let newRecipe = {
+            name: document.querySelector("#newRecipeName").value,
+            price: document.querySelector("#newRecipePrice").value,
+            ingredients: []
+        }
+
+        let inputs = document.querySelectorAll("#recipeInputIngredients > div");
+        for(let input of inputs){
+            newRecipe.ingredients.push({
+                ingredient: input.children[1].children[0].value,
+                quantity: input.children[2].children[0].value
+            });
+        }
+
+        if(!validator.recipe(newRecipe)){
+            return;
+        }
+
+        fetch("/recipe/create", {
+            method: "POST",
+            headers: {
+                "Content-Type": "application/json;charset=utf-8"
+            },
+            body: JSON.stringify(newRecipe)
+        })
+            .then((response)=>{
+                if(typeof(response.data) === "string"){
+                    banner.createError(response.data);
+                }else{
+                    banner.createNotification("New recipe successfully created")
+                }
+            })
+            .catch((err)=>{
+                banner.createError("Refresh page to update data");
+            });
+    }
+}

+ 0 - 1
views/dataPage/purchase.js

@@ -15,7 +15,6 @@ window.purchaseObj = {
             let purchasesDiv = document.querySelector("#purchaseOptions");
 
             for(let item of data.merchant.inventory){
-                console.log(item);
                 let checkDiv = document.createElement("div");
                 checkDiv.classList = "checkboxDiv";
                 purchasesDiv.appendChild(checkDiv);

+ 1 - 1
views/informationPage/information.ejs

@@ -485,7 +485,7 @@
         </div>
 
         <div id="helpStrand" class="strand">
-            <h1>Found a bug?  Need help?  Have a suggestion?</h1>
+            <h1>Found a bug?  Need help?  Have a suggestion? Need to resest your Password?</h1>
 
             <h1>Send us an email at <a href="mailto:lee@thesubline.com">lee@thesubline.com</a></h1>
         </div>

+ 0 - 1
views/landingPage/landing.ejs

@@ -111,7 +111,6 @@
             let error = <%- JSON.stringify(error) %>;
             let isLoggedIn = <%- JSON.stringify(isLoggedIn) %>;
         </script>
-        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
         <script src="../shared/validation.js"></script>
         <script src="/landingPage/public.js"></script>
         <script src="/landingPage/login.js"></script>

+ 0 - 0
views/dashboardPage/account.js → views/oldDashboardPage/account.js


+ 0 - 0
views/dashboardPage/addIngredient.js → views/oldDashboardPage/addIngredient.js


+ 215 - 0
views/oldDashboardPage/dashboard.css

@@ -0,0 +1,215 @@
+#title{
+    font-size: 45px;
+    color: rgb(255, 99, 107);
+    text-align: center;
+    margin-top: 25px;
+}
+
+.cursor{
+    cursor: pointer;
+}
+
+/* Inventory Strand */
+#inventoryStrand{
+    flex-direction: column;
+    align-items: center;
+}
+
+    #inventoryStrand > *{
+        margin: 10px;
+    }
+
+    #inventoryStrand tbody{
+        text-transform: capitalize;
+    }
+
+    #filter{
+        max-height: 32px;
+    }
+
+/* Recipes Strand */
+#recipesStrand{
+    flex-direction: column;
+    align-items: center;
+}
+
+    #recipesStrand > *{
+        margin: 15px;
+    }
+
+    #recipeUpdate{
+        margin-bottom: 10px;
+    }
+
+    #newRecipe{
+        display: none;
+        justify-content: center;
+    }
+
+    #recipesContainer{
+        display: flex;
+        justify-content: space-around;
+        flex-wrap: wrap;
+        padding: 25px;
+    }
+
+        .recipe-card{
+            border: 2px solid rgb(255, 99, 107);
+            box-shadow: 2px 2px 2px rgb(0, 27, 45);
+            background: rgb(0, 27, 45);
+            color: rgb(255, 99, 107);
+            width: 20%;
+            min-width: 200px;
+            padding: 35px;
+            margin: 25px;
+            border-radius: 10px;
+            cursor: pointer;
+        }
+
+            .recipe-card:hover{
+                transform: translateY(-3px);
+            }
+
+            .empty-recipe{
+                background: rgb(255, 99, 107);
+                border: 2px solid rgb(0, 27, 45);
+                color: rgb(0, 27, 45);
+            }
+
+/* Account Strand */
+#accountStrand{
+    flex-direction: column;
+    align-items: center;
+}
+
+    #accountStrand > *{
+        margin: 15px;
+    }
+
+    #accountStrand label{
+        display: flex;
+    }
+
+    #accountEdit{
+        display: none;
+    }
+
+    #passwordEdit{
+        display: none;
+    }
+
+/* Add Ingredient Action */
+#addIngredientAction{
+    flex-direction: column;
+    align-items: center;
+}
+
+    #addIngredientAction > *{
+        margin: 25px;
+    }
+
+    .container{
+        display: flex;
+        justify-content: space-around;
+        align-items: center;
+    }
+
+        .container > *{
+            margin: 50px;
+        }
+
+        .container > div{
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+        }
+
+            .container > div > *{
+                margin: 10px;
+            }
+
+/* Enter Transactions Action */
+#enterTransactionsAction{
+    flex-direction: column;
+    align-items: center;
+}
+    #enterTransactionsAction > *{
+        margin: 10px;
+    }
+
+/* Enter Purchase Action */
+#enterPurchaseAction{
+    flex-direction: column;
+    align-items: center;
+}
+
+    #enterPurchaseAction > *{
+        margin: 10px;
+    }
+
+/* Single Recipe Action */
+#singleRecipeAction{
+    display: none;
+    flex-direction: column;
+    margin: auto;
+    align-items: center;
+}
+
+    #singleRecipeAction > *{
+        margin: 15px;
+    }
+
+    #delRecipe{
+        display: "none"
+    }
+
+@media screen and (max-width: 1000px){
+    /* General use */
+    body{
+        text-align: center;
+    }
+
+    /* Single Recipe Action */
+    #singleRecipeAction .buttonsDiv{
+        font-size: 15px;
+    }
+
+    #singleRecipeAction input{
+        max-width: 100px;
+    }
+}
+
+@media screen and (max-width: 600px){
+    /* Inventory Strand */
+    #inventoryStrand h1{
+        font-size: 20px;
+        text-align: center;
+        margin: 5px;
+    }
+
+    .options{
+        flex-direction: column;
+    }
+
+    .options > *{
+        margin: 5px;
+    }
+
+    #inventoryStrand .button-small{
+        font-size: 12px;
+    }
+
+    #inventoryStrand td, #inventoryStrand th{
+        padding: 3px;
+        font-size: 12px;
+    }
+
+    /* addIngredientStrand */
+    .container{
+        flex-direction: column;
+    }
+
+    #addIngredientAction > *{
+        margin: 10px;
+    }
+}

+ 256 - 0
views/oldDashboardPage/dashboard.ejs

@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="UTF-8">
+        <title>The Subline</title>
+        <link rel="icon" type="img/png" href="/shared/images/logo.png">
+        <link rel="stylesheet" href="/dashboardPage/dashboard.css">
+        <link rel="stylesheet" href="/shared/shared.css">
+    </head>
+    <body>
+        <% include ../shared/header %>
+
+        <% include ../shared/banner %>
+
+        <h1 id="title"><%=merchant.name%></h1>
+
+        <strand-selector></strand-selector>
+
+        <div id="inventoryStrand" class="strand">
+            <div class="buttonBox">
+                <button class="button" onclick="addIngredientObj.display()">Add Ingredient</button>
+
+                <button class="button" onclick="enterPurchaseObj.display()">Enter Purchases</button>
+
+                <% if(merchant.pos === "none"){ %>
+                    <button class="button" onclick="enterTransactionsObj.display()">Enter Transactions</button>
+                <% } %>
+
+                <a class="button" href="/data">Analyze data</a>
+            </div>
+
+            <input id="filter" onkeyup="inventoryObj.filter()" type="text" placeholder="FILTER INGREDIENTS">
+
+            <table>
+                <thead>
+                    <tr>
+                        <th class="cursor" onclick="inventoryObj.sortIngredients('name')">Item</th>
+                        <th class="cursor" onclick="inventoryObj.sortIngredients('category')">Category</th>
+                        <th class="cursor" onclick="inventoryObj.sortIngredients('quantity')">Quantity</th>
+                        <th class="cursor" onclick="inventoryObj.sortIngredients('unit')">Unit</th>
+                        <th>Actions</th>
+                    </tr>
+                    <tbody></tbody>
+                </thead>
+            </table>
+        </div>
+
+        <div id="recipesStrand" class="strand">
+            <div>
+                <% if(merchant.pos === "none"){ %>
+                    <button class="button" onclick="window.recipesObj.showInput()">Add a Recipe</button>
+                <% }else{%>
+                    <button class="button" id="recipeUpdate" onclick="recipesObj.updateRecipes()">Update Recipes</button>
+                <% } %>
+            </div>
+
+            <div id="newRecipe">
+                <label>Name:
+                    <input id="newName" type="text">
+                </label>
+
+                <label>Price:
+                    <input id="newPrice" type="number" step="0.01">
+                </label>
+
+                <button class="button-small" onclick="window.recipesObj.submitNew()">Create</button>
+
+                <button class="button-small" onclick="window.recipesObj.cancelAdd()">Cancel</button>
+            </div>
+            
+            <div id="recipesContainer"></div>
+        </div>
+
+        <div id="accountStrand" class="strand">
+            <form id="accountDisplay" onsubmit="accountObj.editAccount()">
+                <label>Name:&nbsp;&nbsp;
+                    <p><%=merchant.name%></p>
+                </label>
+
+                <label>Email:&nbsp;&nbsp;
+                    <p><%=merchant.email%></p>
+                </label>
+
+                <button class="button" onclick="accountObj.editAccount()">Edit</button>
+            </form>
+
+            <form id="accountEdit">
+                <label>Name:&nbsp;&nbsp;
+                    <input id="accountName" type="text" value="<%=merchant.name%>">
+                </label>
+
+                <label>Email:&nbsp;&nbsp;
+                    <input id="accountEmail" type="email" value="<%=merchant.email%>">
+                </label>
+
+                <div class="buttonBox">
+                    <button class="button" onclick="accountObj.updateAccount()">Save</button>
+
+                    <button class="button" onclick="accountObj.editAccountCancel()">Cancel</button>
+                </div>
+            </form>
+
+            <form id="passwordEdit" onsubmit="accountObj.updatePassword()">
+                <label>Old password:
+                    <input id="oldPass" type="password" required>
+                </label>
+
+                <label>New password:
+                    <input id="newPass" type="password" required>
+                </label>
+
+                <label>Confirm new password:
+                    <input id="confirmNewPass" type="password" required>
+                </label>
+
+
+                <div class="buttonBox">
+                    <input class="button" type="submit" value="Submit">
+
+                    <button class="button" onclick="accountObj.editPasswordCancel()">Cancel</button>
+                </div>
+            </form>
+
+            <button class="button" onclick="accountObj.editPassword()">Change password</button>
+        </div>
+
+
+        <div id="addIngredientAction" class="action">
+            <div class="container">
+                <div>
+                    <h2>Choose Ingredients</h2>
+
+                    <input id="addFilter" type="text" placeholder="FILTER INGREDIENTS" onkeyup="window.addIngredientObj.filterAndDisplay()">
+
+                    <table>
+                        <thead>
+                            <tr>
+                                <th>Add</th>
+                                <th class="cursor" onclick="window.addIngredientObj.sort(1)">Ingredient</th>
+                                <th class="cursor" onclick="window.addIngredientObj.sort(2)">Category</th>
+                                <th class="cursor" onclick="window.addIngredientObj.sort(3)">Unit</th>
+                                <th>Quantity</th>
+                            </tr>
+                        </thead>
+                        <tbody></tbody>
+                    </table>
+
+                    <button class="button" onclick="addIngredientObj.submitAdd()">Submit</button>
+                </div>
+
+                <h1>Or</h1>
+
+                <form id="createIngredientInput" onsubmit="addIngredientObj.submitNew()">
+                    <h2>Create New Ingredient</h2>
+
+                    <label>Name
+                        <input id="newIngName" type="text" required>
+                    </label>
+
+                    <label>Category
+                        <input id="newCategory" type="text" required>
+                    </label>
+
+                    <label>Unit
+                        <input id="newUnit" type="text" required>
+                    </label>
+
+                    <label>Quantity
+                        <input id="newQuantity" type="number" step="0.01" required>
+                    </label>
+
+                    <button class="button">Create Ingredient</button>
+                </form>
+            </div>
+        </div>
+
+
+        <div id="enterTransactionsAction" class="action">
+            <h1>Enter all sales</h1>
+            <h3>Last updated: <span id="updated"></span></h3>
+            
+            <table>
+                <thead>
+                    <tr>
+                        <th>Recipe</th>
+                        <th>Number sold</th>
+                    </tr>
+                </thead>
+                <tbody></tbody>
+            </table>
+
+            <button class="button" onclick="enterTransactionsObj.submit()">Submit</button>
+        </div>
+
+
+        <div id="enterPurchaseAction" class="action">
+            <h1>Enter Purchases</h1>
+            
+            <table>
+                <thead>
+                    <tr>
+                        <th>Ingredient</th>
+                        <th>Amount</th>
+                    </tr>
+                </thead>
+                <tbody></tbody>
+            </table>
+
+            <button class="button" onclick="enterPurchaseObj.submit()">Submit</button>
+        </div>
+
+
+        <div id="singleRecipeAction" class="action">
+            <h2 id="recipeName"></h2>
+
+            <div class="buttonBox">
+                <button class="button" id="addButton">Add Ingredient</button>
+                <% if(merchant.pos === "none"){ %>
+                    <button class="button" id="removeButton">Remove</button>
+                <% } %>
+            </div>
+
+            <table>
+                <thead>
+                    <tr>
+                        <th>Name</th>
+                        <th>Quantity</th>
+                        <th>Actions</th>
+                    </tr>
+                </thead>
+                <tbody></tbody>
+            </table>
+        </div>
+
+        <%- include ../shared/footer %>
+
+        <script>
+                <% if(locals.error){ %>
+                    let error = <%- JSON.stringify(error) %>;
+                <% }else{ %>
+                    let error = undefined;
+                <% } %>
+        </script>
+        <script>let merchant = <%- JSON.stringify(merchant) %>;</script>
+        <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
+        <script src="../shared/validation.js"></script>
+        <script src="/dashboardPage/inventory.js"></script>
+        <script src="/dashboardPage/recipes.js"></script>
+        <script src="/dashboardPage/account.js"></script>
+        <script src="/dashboardPage/addIngredient.js"></script>
+        <script src="/dashboardPage/enterTransactions.js"></script>
+        <script src="/dashboardPage/enterPurchase.js"></script>
+        <script src="/dashboardPage/singleRecipe.js"></script>
+        <script src="/shared/controller.js"></script>
+    </body>
+</html>

+ 0 - 0
views/dashboardPage/enterPurchase.js → views/oldDashboardPage/enterPurchase.js


+ 0 - 0
views/dashboardPage/enterTransactions.js → views/oldDashboardPage/enterTransactions.js


+ 0 - 0
views/dashboardPage/inventory.js → views/oldDashboardPage/inventory.js


+ 0 - 0
views/dashboardPage/recipes.js → views/oldDashboardPage/recipes.js


+ 0 - 0
views/dashboardPage/singleRecipe.js → views/oldDashboardPage/singleRecipe.js


+ 10 - 0
views/passResetPage/passReset.css

@@ -0,0 +1,10 @@
+form > *{
+    margin: 15px;
+}
+
+.content{
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-top: 15vh;
+}

+ 56 - 0
views/passResetPage/passReset.ejs

@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8">
+        <title>The Subline</title>
+        <link rel="icon" type="img/png" href="/shared/images/logo.png">
+        <link rel="stylesheet" href="/passResetPage/passReset.css">
+        <link rel="stylesheet" href="/shared/shared.css">
+    </head>
+    <body>
+        <% include ../shared/header %>
+
+        <% include ../shared/banner %>
+
+        <div class="content">
+            <h1>Reset Password</h1>
+
+            <form action="/resetpassword" method="post" onsubmit="submitPass()">
+                <label>New Password:
+                    <input type="password" id="pass" name="pass" required>
+                </label>
+
+                <label>Confirm Password:
+                    <input type="password" id="confirmPass" name="confirmPass" required>
+                </label>
+
+                <input class="button" type="submit" value="Submit">
+            </form>
+        </div>
+
+        <script src="../shared/validation.js"></script>
+        <script>
+            let submitPass = ()=>{
+                event.preventDefault();
+
+                let pass = document.querySelector("#pass").value;
+                let confirmPass = document.querySelector("#confirmPass").value;
+
+                if(validator.merchant.password(pass, confirmPass)){
+                    let url = window.location.href;
+                    let hash = url.slice(url.indexOf("resetpassword/") + 14);
+                    
+                    let form = document.querySelector("form");
+
+                    let hidden = document.createElement("input");
+                    hidden.type = "hidden";
+                    hidden.name = "hash";
+                    hidden.value = hash;
+                    form.appendChild(hidden);
+
+                    form.submit();
+                }
+            }
+        </script>
+    </body>
+</html>

+ 80 - 19
views/shared/graphs.js

@@ -5,20 +5,15 @@
 //  yName = a string for the name of the Y axis
 //  xName = a string for the name of the X axis
 class LineGraph{
-    constructor(canvas, yName, xName){
-        canvas.height = canvas.clientHeight;
-        canvas.width = canvas.clientWidth;
-
+    constructor(canvas){
         this.canvas = canvas;
         this.context = canvas.getContext("2d");
-        this.left = canvas.clientWidth - (canvas.clientWidth * 0.9);
-        this.right = canvas.clientWidth * 0.8;
-        this.top = canvas.clientHeight - (canvas.clientHeight * 0.99);
-        this.bottom = canvas.clientHeight * 0.9;
+        this.left = canvas.clientWidth - (canvas.clientWidth * 0.95);
+        this.right = canvas.clientWidth * 1;
+        this.top = canvas.clientHeight - (canvas.clientHeight * 1);
+        this.bottom = canvas.clientHeight * 0.85;
         this.data = [];
         this.max = 0;
-        this.xName = xName;
-        this.yName = yName;
         this.xRange = [];
         this.colors = [];
         this.colorIndex = 0;
@@ -39,7 +34,6 @@ class LineGraph{
     //  xRange = array containing two elements, start and end for x axis data (currently only dates)
     //  name = string name for the line.  Used for display and finding lines.  Each must be unique
     addData(data, xRange, name){
-        console.log(data);
         data = {
             set: data,
             colorIndex: this.colorIndex,
@@ -101,18 +95,30 @@ class LineGraph{
         this.xRange = [];
     }
 
+    addTitle(title){
+        this.top = this.canvas.clientHeight - (this.canvas.clientHeight * 0.9);
+        
+        this.title = title;
+    }
+
     /**********
     *********PRIVATE*********
     **********/
     drawGraph(){
         this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
-
+        
         this.drawYAxis();
         this.drawXAxis();
 
         for(let dataSet of this.data){
             this.drawLine(dataSet);
         }
+
+        if(this.title){
+            this.context.font = "25px Saira";
+            let xLocation = ((this.right - this.left) / 2) - (this.context.measureText(this.title).width / 2);
+            this.context.fillText(this.title, xLocation, this.top - 10);
+        }
     }
 
     drawLine(data){
@@ -127,7 +133,9 @@ class LineGraph{
 
         this.context.strokeStyle = "black";
 
-        this.drawLegend(data.colorIndex, data.name);
+        if(this.data.length > 1){
+            this.drawLegend(data.colorIndex, data.name);
+        }
     }
 
     drawXAxis(){
@@ -137,9 +145,6 @@ class LineGraph{
         this.context.lineWidth = 4;
         this.context.stroke();
 
-        this.context.font = "25px Arial";
-        this.context.fillText(this.xName, this.right / 2, this.bottom + 50);
-
         this.context.setLineDash([5, 10]);
         this.context.font = "10px Arial";
         this.context.lineWidth = 1;
@@ -175,9 +180,6 @@ class LineGraph{
         this.context.lineWidth = 2;
         this.context.stroke();
 
-        this.context.font = "25px Arial";
-        this.context.fillText(this.yName, 0, this.bottom / 2);
-
         this.context.setLineDash([5, 10]);
         this.context.font = "10px Arial";
         this.context.lineWidth = 1;
@@ -221,4 +223,63 @@ class LineGraph{
 
         this.context.fillStyle = "black";
     }
+}
+
+class HorizontalBarGraph{
+    constructor(canvas){
+        this.canvas = canvas;
+        this.context = canvas.getContext("2d");
+        this.left = 0;
+        this.right = canvas.clientWidth;
+        this.top = canvas.clientHeight - (canvas.clientHeight * 0.99);
+        this.bottom = canvas.clientHeight;
+        this.data = [];
+        this.max = 0;
+    }
+
+    //Adds an array of data points to the chart
+    //All data is removed  and redrawn when called
+    //Must pass in all data points
+    //Inputs: 
+    //  dataArray: array of objects
+    //      num: number for the actual data
+    //      label: text to display on bar
+    addData(dataArray){
+        let data = [];
+        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+        for(let point of dataArray){
+            if(point.num > this.max){
+                this.max = point.num;
+            }
+
+            this.data.push(point);
+        }
+
+        this.drawGraph();
+    }
+
+    drawGraph(){
+        let barHeight = (this.bottom - this.top) / this.data.length;
+
+        for(let i = 0; i < this.data.length; i++){
+            let topLocation = this.top + (i * barHeight) + 5;
+            let width = (this.right - this.left) * (this.data[i].num / this.max);
+
+            if(this.data[i].num >= this.max){
+                this.context.fillStyle = "rgb(255, 99, 107)";
+            }else{
+                this.context.fillStyle = "rgb(179, 191, 209)";
+            }
+
+            this.context.beginPath();
+            this.context.fillRect(this.left, topLocation, width, barHeight - 5);
+            this.context.stroke();
+
+            let textLocation  = 15;
+            this.context.font = "12px Saira";
+            this.context.fillStyle = "black";
+            this.context.fillText(this.data[i].label, textLocation, (this.top) + (i * barHeight) + (barHeight / 1.5));
+        }
+    }
 }

BIN
views/shared/images/downArrow.png


BIN
views/shared/images/homeIcon.png


BIN
views/shared/images/profile.png


BIN
views/shared/images/upArrow.png


+ 0 - 0
views/shared/controller.js → views/shared/oldController.js


+ 10 - 5
views/shared/shared.css

@@ -1,13 +1,9 @@
 /* Initialization for all pages */
-*{
-    margin:0;
-    padding:0;
-}
+*{margin:0;padding:0;font-family:'Saira',sans-serif;}
 
 body{
     display: flex;
     flex-direction: column;
-    font-family:'Saira',sans-serif;
     color: rgb(0, 27, 45);
     min-height: 100vh;
 }
@@ -23,6 +19,15 @@ h3 {
     text-align: center;
     font-weight: 500;
     color: gray;
+    font-size: 20px;
+}
+
+button{
+    outline: 0;
+}
+
+button::-moz-focus-inner{
+    border: 0;
 }
 
 .link {

+ 42 - 0
views/shared/validation.js

@@ -137,6 +137,48 @@ let validator = {
         }
     },
 
+    recipe: function(newRecipe, createBanner = true){
+        let errors = [];
+
+        if(!validator.isSanitary(newRecipe.name)){
+            errors.push("Name contains invalid characters");
+        }
+
+        if(newRecipe.price < 0){
+            errors.push("Price must contain a non-negative number");
+        }
+
+        if(newRecipe.ingredients.length === 0){
+            errors.push("Must include at least one ingredient");
+        }
+
+        let checkSet = new Set();
+        for(let ingredient of newRecipe.ingredients){
+            if(ingredient.quantity < 0){
+                errors.push("Quantity must contain a non-negative number");
+                break;
+            }
+
+            checkSet.add(ingredient.ingredient);
+        }
+
+        if(checkSet.size !== newRecipe.ingredients.length){
+            errors.push("Recipe contains duplicate ingredients");
+        }
+
+        if(errors.length > 0){
+            if(createBanner){
+                for(let error of errors){
+                    banner.createError(error);
+                }
+
+                return false;
+            }
+        }
+
+        return true;
+    },
+
     isSanitary: function(str){
         let disallowed = ["\\", "<", ">", "$", "{", "}", "(", ")"];