瀏覽代碼

Merge branch 'development'

Lee Morgan 5 年之前
父節點
當前提交
4d1ac0c363
共有 47 個文件被更改,包括 1848 次插入779 次删除
  1. 1 2
      .gitignore
  2. 39 11
      controllers/emailVerification.js
  3. 38 0
      controllers/helper.js
  4. 161 43
      controllers/ingredientData.js
  5. 89 79
      controllers/merchantData.js
  6. 175 6
      controllers/orderData.js
  7. 2 4
      controllers/otherData.js
  8. 8 2
      controllers/passwordReset.js
  9. 201 28
      controllers/recipeData.js
  10. 192 6
      controllers/transactionData.js
  11. 0 161
      controllers/validator.js
  12. 2 4
      emails/verifyEmail.js
  13. 17 13
      models/ingredient.js
  14. 23 7
      models/merchant.js
  15. 18 15
      models/order.js
  16. 14 6
      models/recipe.js
  17. 3 3
      models/transaction.js
  18. 202 11
      package-lock.json
  19. 3 1
      package.json
  20. 15 5
      routes.js
  21. 0 0
      views/dashboardPage/bundle.js
  22. 58 0
      views/dashboardPage/dashboard.css
  23. 15 2
      views/dashboardPage/dashboard.ejs
  24. 55 22
      views/dashboardPage/js/classes/Merchant.js
  25. 0 15
      views/dashboardPage/js/classes/Order.js
  26. 2 1
      views/dashboardPage/js/classes/Recipe.js
  27. 18 0
      views/dashboardPage/js/classes/Transaction.js
  28. 48 2
      views/dashboardPage/js/dashboard.js
  29. 3 6
      views/dashboardPage/js/sidebars/editIngredient.js
  30. 37 14
      views/dashboardPage/js/sidebars/newIngredient.js
  31. 40 11
      views/dashboardPage/js/sidebars/newOrder.js
  32. 45 4
      views/dashboardPage/js/sidebars/newRecipe.js
  33. 39 8
      views/dashboardPage/js/sidebars/newTransaction.js
  34. 1 1
      views/dashboardPage/js/sidebars/orderDetails.js
  35. 170 225
      views/dashboardPage/js/strands/analytics.js
  36. 12 24
      views/dashboardPage/js/strands/home.js
  37. 7 8
      views/dashboardPage/js/strands/orders.js
  38. 18 0
      views/dashboardPage/modal.ejs
  39. 5 4
      views/dashboardPage/sidebars.css
  40. 5 1
      views/dashboardPage/sidebars/newIngredient.ejs
  41. 2 0
      views/dashboardPage/sidebars/newOrder.ejs
  42. 2 0
      views/dashboardPage/sidebars/newRecipe.ejs
  43. 2 0
      views/dashboardPage/sidebars/newTransaction.ejs
  44. 34 6
      views/informationPages/help.ejs
  45. 21 8
      views/shared/shared.css
  46. 1 1
      views/verifyPage/verify.css
  47. 5 9
      views/verifyPage/verify.ejs

+ 1 - 2
.gitignore

@@ -1,3 +1,2 @@
 node_modules/
-dist/
-package-lock.json
+dist/

+ 39 - 11
controllers/emailVerification.js

@@ -2,6 +2,7 @@ const Merchant = require("../models/merchant.js");
 
 const mailgun = require("mailgun-js")({apiKey: process.env.MG_SUBLINE_APIKEY, domain: "mail.thesubline.net"});
 const verifyEmail = require("../emails/verifyEmail.js");
+const { db } = require("../models/merchant.js");
 
 module.exports = {
     sendVerifyEmail: function(req, res){
@@ -13,13 +14,13 @@ module.exports = {
                     subject: "Email verification",
                     html: verifyEmail({
                         name: merchant.name,
-                        link: `${process.env.SITE}/verify/${merchant._id}`,
-                        code: merchant.verifyId
+                        link: `${process.env.SITE}/verify/${merchant._id}/${merchant.verifyId}`,
                     })
                 };
                 mailgun.messages().send(mailgunData, (err, body)=>{});
 
-                return res.redirect(`/verify/${merchant._id}`);
+
+                return res.render(`verifyPage/verify`, {id: merchant._id, email: merchant.email});
             })
             .catch((err)=>{
                 req.session.error = "ERROR: UNABLE TO SEND VERIFICATION EMAIL";
@@ -27,17 +28,40 @@ module.exports = {
             });
     },
 
-    verifyPage: function(req, res){
-        return res.render("verifyPage/verify", {id: req.params.id});
+    resendEmail: function(req, res){
+        Merchant.findOne({email: req.body.email.toLowerCase()})
+            .then((merchant)=>{
+                if(merchant){
+                    throw "USER WITH THIS EMAIL ADDRESS ALREADY EXISTS";
+                }
+
+                return Merchant.findOne({_id: req.body.id});
+            })
+            .then((merchant)=>{
+                merchant.email = req.body.email.toLowerCase();
+
+                return merchant.save();
+            })
+            .then((merchant)=>{
+                return res.redirect(`/verify/email/${merchant._id}`);
+            })
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    req.session.error = err;
+                }else if(err.name === "ValidationError"){
+                    req.session.error = err.errors[Object.keys(err.errors)[0]].properties.message;
+                }else{
+                    req.session.error = "ERROR: UNABLE TO CHANGE YOUR EMAIL ADDRESS";
+                }
+                return res.redirect("/");
+            });
     },
 
     verify: function(req, res){
-        Merchant.findOne({_id: req.body.id})
+        Merchant.findOne({_id: req.params.id})
             .then((merchant)=>{
-                if(req.body.code !== merchant.verifyId){
-                    req.session.error = "INCORRECT CODE";
-                    
-                    return res.redirect(`/verify/${merchant._id}`);
+                if(req.params.code !== merchant.verifyId){
+                    throw "UNABLE TO VERIFY EMAIL ADDRESS.  INCORRECT LINK";
                 }
 
                 merchant.verifyId = undefined;
@@ -60,7 +84,11 @@ module.exports = {
                 return res.redirect("/dashboard");
             })
             .catch((err)=>{
-                req.session.error = "ERROR: UNABLE TO VERIFY EMAIL ADDRESS";
+                if(typeof(err) === "string"){
+                    req.session.error = err;
+                }else{
+                    req.session.error = "ERROR: UNABLE TO VERIFY EMAIL ADDRESS"
+                }
 
                 return res.redirect("/");
             });

+ 38 - 0
controllers/helper.js

@@ -235,6 +235,30 @@ module.exports = {
         }
     },
 
+    convertPrice: function(price, unit){
+        switch(unit){
+            case "g":return price; 
+            case "kg": return price / 1000;
+            case "oz": return price / 28.3495;
+            case "lb": return price / 453.5924;
+            case "ml": return price * 1000;
+            case "l": return price;
+            case "tsp": return price * 202.8842;
+            case "tbsp": return price * 67.6278;
+            case "ozfl": return price * 33.8141;
+            case "cup": return price * 4.1667;
+            case "pt": return price * 2.1134;
+            case "qt": return price * 1.0567;
+            case "gal": return price / 3.7854;
+            case "mm": return price * 1000;
+            case "cm": return price * 100;
+            case "m": return price;
+            case "in": return price * 39.3701;
+            case "ft": return price * 3.2808;
+            default: return price;
+        }
+    },
+
     generateId: function(length){
         let result = "";
         let characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
@@ -244,5 +268,19 @@ module.exports = {
         }
         
         return result;
+    },
+
+    isSanitary: function(strings){
+        let disallowed = ["\\", "<", ">", "$", "{", "}", "(", ")"];
+
+        for(let i = 0; i < strings.length; i++){
+            for(let j = 0; j < disallowed.length; j++){
+                if(strings[i].includes(disallowed[j])){
+                    return false;
+                }
+            }
+        }
+
+        return true;
     }
 }

+ 161 - 43
controllers/ingredientData.js

@@ -2,23 +2,12 @@ const Merchant = require("../models/merchant");
 const Ingredient = require("../models/ingredient");
 const InventoryAdjustment = require("../models/inventoryAdjustment.js");
 
-const Helper = require("./helper.js");
-const Validator = require("./validator.js");
+const helper = require("./helper.js");
 
-module.exports = {
-    //GET - gets a list of all database ingredients
-    //Returns:
-    //  ingredients: list containing all ingredients
-    getIngredients: function(req, res){
-        Ingredient.find()
-            .then((ingredients)=>{
-                return res.json(ingredients);
-            })
-            .catch((err)=>{
-                return res.json("ERROR: UNABLE TO RETRIEVE INGREDIENTS");
-            });
-    },
+const xlsx = require("xlsx");
+const fs = require("fs");
 
+module.exports = {
     /*
     POST - create a single ingredient and then add to the merchant
     req.body = {
@@ -39,23 +28,6 @@ module.exports = {
             return res.redirect("/");
         }
 
-        let validation = Validator.ingredient(req.body.ingredient);
-        if(validation !== true){
-            return res.json(validation);
-        }
-
-        validation = Validator.quantity(req.body.quantity);
-        if(validation !== true){
-            return res.json(validation);
-        }
-
-        if(req.body.ingredient.unitSize){
-            validation = Validator.quantity(req.body.ingredient.unitSize);
-            if(validation !== true){
-                return res.json(validation);
-            }
-        }
-
         let newIngredient = {};
         if(req.body.ingredient.specialUnit === "bottle"){
             newIngredient = new Ingredient({
@@ -63,7 +35,7 @@ module.exports = {
                 category: req.body.ingredient.category,
                 unitType: req.body.ingredient.unitType,
                 specialUnit: req.body.ingredient.specialUnit,
-                unitSize: Helper.convertQuantityToBaseUnit(req.body.ingredient.unitSize, req.body.defaultUnit)
+                unitSize: helper.convertQuantityToBaseUnit(req.body.ingredient.unitSize, req.body.defaultUnit)
             });
         }else{
             newIngredient = new Ingredient(req.body.ingredient);
@@ -82,7 +54,7 @@ module.exports = {
                 if(response[0].specialUnit === "bottle"){
                     newIngredient.quantity = req.body.quantity * response[0].unitSize;
                 }else{
-                    newIngredient.quantity = Helper.convertQuantityToBaseUnit(req.body.quantity, req.body.defaultUnit);
+                    newIngredient.quantity = helper.convertQuantityToBaseUnit(req.body.quantity, req.body.defaultUnit);
                 }
 
                 response[1].inventory.push(newIngredient);
@@ -93,7 +65,13 @@ module.exports = {
                 return res.json(newIngredient);
             })
             .catch((err)=>{
-                return res.json("ERROR: UNABLE TO CREATE NEW INGREDIENT");
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO CREATE THE INGREDIENT");
             });
     },
 
@@ -113,17 +91,12 @@ module.exports = {
             req.session.error = "MUST BE LOGGED IN TO DO THAT";
             return res.redirect("/");
         }
-
-        const ingredientCheck = Validator.ingredient(req.body);
-        if(ingredientCheck !== true){
-            return res.json(ingredientCheck);
-        }
-
         let updatedIngredient = {};
         Ingredient.findOne({_id: req.body.id})
             .then((ingredient)=>{
                 ingredient.name = req.body.name,
                 ingredient.category = req.body.category
+                //TODO: Handle this in the class
                 if(ingredient.specialUnit === "bottle"){
                     ingredient.unitSize = req.body.unitSize;
                 }
@@ -150,7 +123,7 @@ module.exports = {
                             merchant.inventory[i].quantity = req.body.quantity;
                         }
 
-                        updatedIngredient.quantity = req.body.quantity;
+                        updatedIngredient.quantity = helper.convertQuantityToBaseUnit(req.body.quantity, req.body.unit);
                         updatedIngredient.unit = req.body.unit;
                         
                         break;
@@ -163,10 +136,149 @@ module.exports = {
                 return res.json(updatedIngredient);
             })
             .catch((err)=>{
-                return res.json("ERROR: UNABLE TO UPDATE INGREDIENT");
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO UDATE THE INGREDIENT");
+            });
+    },
+
+    createFromSpreadsheet: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        //read file, get the correct sheet, create array from sheet
+        let workbook = xlsx.readFile(req.file.path);
+        fs.unlink(req.file.path, ()=>{});
+
+        let sheets = Object.keys(workbook.Sheets);
+        let sheet = {};
+        for(let i = 0; i < sheets.length; i++){
+            let str = sheets[i].toLowerCase();
+            if(str === "ingredient" || str === "ingredients"){
+                sheet = workbook.Sheets[sheets[i]];
+            }
+        }
+        const array = xlsx.utils.sheet_to_json(sheet, {
+            header: 1
+        });
+
+        //get property locations
+        let locations = {};
+        for(let i = 0; i < array[0].length; i++){
+            switch(array[0][i].toLowerCase()){
+                case "name": locations.name = i; break;
+                case "category": locations.category = i; break;
+                case "quantity": locations.quantity = i; break;
+                case "unit": locations.unit = i; break;
+                case "bottle": locations.bottle = i; break;
+                case "bottle size": locations.bottleSize = i; break;
+            }
+        }
+
+        //Create ingredients
+        let ingredients = [];
+        let merchantData = [];
+        for(let i = 1; i < array.length; i++){
+            let ingredient = new Ingredient({
+                name: array[i][locations.name],
+                category: array[i][locations.category]
+            });
+
+            let merchantItem = {
+                ingredient: ingredient,
+                quantity: helper.convertQuantityToBaseUnit(array[i][locations.quantity], array[i][locations.unit]),
+                defaultUnit: array[i][locations.unit]
+            }
+
+            if(array[i][locations.bottle] === true){
+                ingredient.unitType = "volume";
+                ingredient.specialUnit = "bottle";
+                ingredient.unitSize = helper.convertQuantityToBaseUnit(array[i][locations.bottleSize], array[i][locations.unit]);
+            }else{
+                let unitType = "";
+                //TODO: this should probably be in a helper
+                switch(array[i][locations.unit].toLowerCase()){
+                    case "g": unitType = "mass"; break;
+                    case "kg": unitType = "mass"; break;
+                    case "oz": unitType = "mass"; break;
+                    case "lb": unitType = "mass"; break;
+                    case "ml": unitType = "volume"; break;
+                    case "l": unitType = "volume"; break;
+                    case "tsp": unitType = "volume"; break;
+                    case "tbsp": unitType = "volume"; break;
+                    case "ozfl": unitType = "volume"; break;
+                    case "cup": unitType = "volume"; break;
+                    case "pt": unitType = "volume"; break;
+                    case "qt": unitType = "volume"; break;
+                    case "gal": unitType = "volume"; break;
+                    case "mm": unitType = "length"; break;
+                    case "cm": unitType = "length"; break;
+                    case "m": unitType = "length"; break;
+                    case "in": unitType = "length"; break;
+                    case "ft": unitType = "length"; break;
+                    default: unitType = "other";
+                }
+
+                ingredient.unitType = unitType;
+            }
+
+            merchantData.push(merchantItem);
+            ingredients.push(ingredient);
+        }
+
+        //Update the database
+        Ingredient.create(ingredients)
+            .then((ingredients)=>{
+                return Merchant.findOne({_id: req.session.user});
+            })
+            .then((merchant)=>{
+                for(let i = 0; i < merchantData.length; i++){
+                    merchant.inventory.push(merchantData[i]);
+                }
+
+                return merchant.save();
+            })
+            .then((merchant)=>{
+                return res.json(merchantData);
+            })
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return "ERROR: UNABLE TO CREATE YOUR INGREDIENTS";
             });
     },
 
+    spreadsheetTemplate: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        let workbook = xlsx.utils.book_new();
+        workbook.SheetNames.push("Ingredients");
+        let workbookData = [];
+
+        workbookData.push(["Name", "Category", "Quantity", "Unit", "Bottle", "Bottle Size"]);
+        workbookData.push(["Example Ingredient 1", "Produce", 100, "lbs"]);
+        workbookData.push(["Example Ingredient Two", "Beverage", 5, "ml", "TRUE", 750]);
+
+        workbook.Sheets.Ingredients = xlsx.utils.aoa_to_sheet(workbookData);
+        xlsx.writeFile(workbook, "SublineIngredients.xlsx");
+        return res.download("SublineIngredients.xlsx", (err)=>{
+            fs.unlink("SublineIngredients.xlsx", ()=>{});
+        });
+    },
+
     //DELETE - Removes an ingredient from the merchant's inventory
     removeIngredient: function(req, res){
         if(!req.session.user){
@@ -192,6 +304,12 @@ module.exports = {
                 return res.json({});
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO RETRIEVE USER DATA");
             });
     },

+ 89 - 79
controllers/merchantData.js

@@ -1,15 +1,15 @@
-const axios = require("axios");
-const bcrypt = require("bcryptjs");
-
 const Merchant = require("../models/merchant");
 const Recipe = require("../models/recipe");
 const InventoryAdjustment = require("../models/inventoryAdjustment");
-const Validator = require("./validator.js");
-const Helper = require("./helper.js");
+
+const helper = require("./helper.js");
+
+const axios = require("axios");
+const bcrypt = require("bcryptjs");
 
 module.exports = {
     /*
-    POST - Create a new merchant with no POS system
+    POST - Create a new merchant with no POS system1
     req.body = {
         name: retaurant name,
         email: registration email,
@@ -19,43 +19,53 @@ module.exports = {
     Redirects to /dashboard
     */
     createMerchantNone: async function(req, res){
-        let validation =  await Validator.merchant(req.body);
-        if(validation !== true){
-            req.session.error = validation;
+        if(req.body.password.length < 10){
+            req.session.error = "PASSWORD MUST CONTAIN AT LEAST 10 CHARACTERS";
             return res.redirect("/");
         }
 
-        if(req.body.password === req.body.confirmPassword){
-            let salt = bcrypt.genSaltSync(10);
-            let hash = bcrypt.hashSync(req.body.password, salt);
-
-            let merchant = new Merchant({
-                name: req.body.name,
-                email: req.body.email.toLowerCase(),
-                password: hash,
-                pos: "none",
-                lastUpdatedTime: Date.now(),
-                createdAt: Date.now(),
-                status: ["unverified"],
-                inventory: [],
-                recipes: [],
-                verifyId: Helper.generateId(15)
-            });
-
-            merchant.save()
-                .then((merchant)=>{
-                    return res.redirect(`/verify/email/${merchant._id}`);
-                })
-                .catch((err)=>{
-                    req.session.error = "ERROR: UNABLE TO CREATE ACCOUNT AT THIS TIME";
-
-                    return res.redirect("/");
-                });
-        }else{
+        if(req.body.password !== req.body.confirmPassword){
             req.session.error = "PASSWORDS DO NOT MATCH";
+            return res.redirect("/");
+        }
 
+        const merchantFind = await Merchant.findOne({email: req.body.email.toLowerCase()});
+        if(merchantFind !== null){
+            req.session.error = "USER WITH THIS EMAIL ADDRESS ALREADY EXISTS";
             return res.redirect("/");
         }
+
+        let salt = bcrypt.genSaltSync(10);
+        let hash = bcrypt.hashSync(req.body.password, salt);
+
+        let merchant = new Merchant({
+            name: req.body.name,
+            email: req.body.email.toLowerCase(),
+            password: hash,
+            pos: "none",
+            lastUpdatedTime: Date.now(),
+            createdAt: Date.now(),
+            status: ["unverified"],
+            inventory: [],
+            recipes: [],
+            verifyId: helper.generateId(15)
+        });
+
+        merchant.save()
+            .then((merchant)=>{
+                return res.redirect(`/verify/email/${merchant._id}`);
+            })
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    req.session.error = err;
+                }else if(err.name === "ValidationError"){
+                    req.session.error = err.errors[Object.keys(err.errors)[0]].properties.message;
+                }else{
+                    req.session.error = "ERROR: UNABLE TO CREATE ACCOUNT AT THIS TIME";
+                }
+                
+                return res.redirect("/");
+            });
     },
 
     /*
@@ -105,7 +115,14 @@ module.exports = {
                 return res.redirect("/dashboard");
             })
             .catch((err)=>{
-                req.session.error = "ERROR: UNABLE TO RETRIEVE DATA FROM CLOVER";
+                if(typeof(err) === "string"){
+                    req.session.error = err;
+                }else if(err.name === "ValidationError"){
+                    req.session.error = err.errors[Object.keys(err.errors)[0]].properties.message;
+                }else{
+                    req.session.error = "ERROR: UNABLE TO RETRIEVE DATA FROM CLOVER";
+                }
+                
                 return res.redirect("/");
             });
     },
@@ -186,32 +203,14 @@ module.exports = {
                 return res.redirect("/dashboard");
             })
             .catch((err)=>{
-                banner.createError("ERROR: UNABLE TO CREATE NEW USER AT THIS TIME");
-            });
-    },
-
-    //PUT - Update the default unit for a single ingredient
-    ingredientDefaultUnit: function(req, res){
-        if(!req.session.user){
-            req.session.error = "MUST BE LOGGED IN TO DO THAT";
-            return res.redirect("/");
-        }
-
-        Merchant.findOne({_id: req.session.user})
-            .then((merchant)=>{
-                for(let i = 0; i < merchant.inventory.length; i++){
-                    if(merchant.inventory[i].ingredient.toString() === req.params.id){
-                        merchant.inventory[i].defaultUnit =req.params.unit;
-                    }
+                if(typeof(err) === "string"){
+                    req.session.error = err;
+                }else if(err.name === "ValidationError"){
+                    req.session.error = err.errors[Object.keys(err.errors)[0]].properties.message;
+                }else{
+                    req.session.error = "ERROR: UNABLE TO CREATE NEW USER";
                 }
-
-                return merchant.save()
-            })
-            .then((merchant)=>{
-                return res.json({});
-            })
-            .catch((err)=>{
-                return res.json("ERROR: UNABLE TO UPDATE DEFAULT UNIT");
+                return res.redirect("/");
             });
     },
 
@@ -228,21 +227,15 @@ module.exports = {
             return res.redirect("/");
         }
 
-        for(let i = 0; i < req.body.length; i++){
-            let validation = Validator.quantity(req.body[i].quantity);
-            if(validation !== true){
-                return res.json(validation);
-            }
-        }
-
         let adjustments = [];
-
+        let changedIngredients = [];
         Merchant.findOne({_id: req.session.user})
+            .populate("inventory.ingredient")
             .then((merchant)=>{
                 for(let i = 0; i < req.body.length; i++){
                     let updateIngredient;
                     for(let j = 0; j < merchant.inventory.length; j++){
-                        if(merchant.inventory[j].ingredient.toString() === req.body[i].id){
+                        if(merchant.inventory[j].ingredient._id.toString() === req.body[i].id){
                             updateIngredient = merchant.inventory[j];
                             break;
                         }
@@ -255,18 +248,25 @@ module.exports = {
                         quantity: req.body[i].quantity - updateIngredient.quantity,
                     }));
 
-                    updateIngredient.quantity = req.body[i].quantity;
+                    updateIngredient.quantity = helper.convertQuantityToBaseUnit(req.body[i].quantity, updateIngredient.defaultUnit);
+                    changedIngredients.push(updateIngredient);
                 }
 
                 return merchant.save();
             })
             .then((newMerchant)=>{
-                res.json({});
+                res.json(changedIngredients);
 
                 InventoryAdjustment.create(adjustments).catch(()=>{});
                 return;
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO UPDATE DATA");
             });        
     },
@@ -280,14 +280,16 @@ module.exports = {
     }
     */
     updatePassword: function(req, res){
-        let validation = Validator.password(req.body.pass, req.body.confirmPass);
-        if(validation !== true){
-            return res.json(validation);
-        }
-
         Merchant.findOne({password: req.body.hash})
             .then((merchant)=>{
                 if(merchant){
+                    if(req.body.pass.length < 10){
+                        throw "PASSWORD MUST CONTAIN AT LEAST 10 CHARACTERS";
+                    }
+                    if(req.body.pass !== req.body.confirmPass){
+                        throw "PASSWORDS DO NOT MATCH";
+                    }
+
                     let salt = bcrypt.genSaltSync(10);
                     let hash = bcrypt.hashSync(req.body.pass, salt);
 
@@ -303,6 +305,14 @@ module.exports = {
                 req.session.error = "PASSWORD SUCCESSFULLY RESET. PLEASE LOG IN";
                 return res.redirect("/");
             })
-            .catch((err)=>{});
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO UPDATE YOUR PASSWORD");
+            });
     }
 }

+ 175 - 6
controllers/orderData.js

@@ -1,8 +1,11 @@
 const Order = require("../models/order.js");
 const Merchant = require("../models/merchant.js");
 
+const helper = require("./helper.js");
+
 const ObjectId = require("mongoose").Types.ObjectId;
-const Validator = require("./validator.js");
+const xlsx = require("xlsx");
+const fs = require("fs");
 
 module.exports = {
     /*
@@ -41,6 +44,12 @@ module.exports = {
                 return res.json(orders);
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO RETRIEVE YOUR ORDERS");
             });
     },
@@ -87,6 +96,12 @@ module.exports = {
                 return res.json(orders);
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO RETRIEVE YOUR TRANSACTIONS");
             });
     },
@@ -109,11 +124,6 @@ module.exports = {
             return res.redirect("/");
         }
 
-        let validation = Validator.order(req.body);
-        if(validation !== true){
-            return res.json(validation);
-        }
-
         let newOrder = new Order(req.body);
         newOrder.merchant = req.session.user;
         newOrder.save()
@@ -121,6 +131,12 @@ module.exports = {
                 res.json(response);
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO SAVE ORDER");
             });
 
@@ -142,6 +158,153 @@ module.exports = {
             .catch(()=>{});
     },
 
+    createFromSpreadsheet: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        //read file, get the correct sheet, create array from sheet
+        let workbook = xlsx.readFile(req.file.path);
+        fs.unlink(req.file.path, ()=>{});
+
+        let sheets = Object.keys(workbook.Sheets);
+        let sheet = {};
+        for(let i = 0; i < sheets.length; i++){
+            let str = sheets[i].toLowerCase();
+            if(str === "order" || str === "orders"){
+                sheet = workbook.Sheets[sheets[i]];
+            }
+        }
+
+        const array = xlsx.utils.sheet_to_json(sheet, {
+            header: 1
+        });
+
+        //get property locations
+        let locations = {};
+        for(let i = 0; i < array[0].length; i++){
+            switch(array[0][i].toLowerCase()){
+                case "name": locations.name = i; break;
+                case "date": locations.date = i; break;
+                case "taxes": locations.taxes = i; break;
+                case "fees": locations.fees = i; break;
+                case "ingredients": locations.ingredients = i; break;
+                case "quantity": locations.quantity = i; break;
+                case "price": locations.price = i; break;
+            }
+        }
+
+        let merchant = {};
+        Merchant.findOne({_id: req.session.user})
+            .populate("inventory.ingredient")
+            .then((response)=>{
+                merchant = response;
+
+                let orders = [];
+                let currentOrder = {};
+                for(let i = 1; i < array.length; i++){
+                    if(array[i].length === 0 || array[i][locations.ingredients] === undefined || array[i][locations.quantity === 0]){
+                        continue;
+                    }
+
+                    if(array[i][locations.name] !== undefined){
+                        currentOrder = {
+                            merchant: req.session.user,
+                            name: array[i][locations.name],
+                            taxes: parseInt(array[i][locations.taxes] * 100),
+                            fees: parseInt(array[i][locations.fees] * 100),
+                            ingredients: []
+                        }
+
+                        if(array[i][locations.date] === undefined){
+                            currentOrder.date = new Date();
+                        }else{
+                            currentOrder.date = new Date(array[i][locations.date]);
+                        }
+
+                        orders.push(currentOrder);
+                    }
+
+                    let exists = false;
+                    for(let j = 0; j < merchant.inventory.length; j++){
+                        if(merchant.inventory[j].ingredient.name.toLowerCase() === array[i][locations.ingredients].toLowerCase()){
+                            const baseQuantity = helper.convertQuantityToBaseUnit(array[i][locations.quantity], merchant.inventory[j].defaultUnit);
+                            currentOrder.ingredients.push({
+                                ingredient: merchant.inventory[j].ingredient._id,
+                                quantity: baseQuantity,
+                                pricePerUnit: helper.convertPrice(array[i][locations.price] * 100, merchant.inventory[j].defaultUnit)
+                            });
+
+                            merchant.inventory[j].quantity += baseQuantity;
+
+                            exists = true;
+                            break;
+                        }
+                    }
+
+                    if(exists === false){
+                        throw `CANNOT FIND INGREDIENT ${array[i][locations.ingredients]} FROM ORDER ${array[i][locations.name]}`;
+                    }
+                }
+
+                return Promise.all([Order.create(orders), merchant.save()]);
+            })
+            .then((response)=>{
+                return res.json(response[0]);
+            })
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO CREATE YOUR ORDERS");
+            });
+    },
+
+    /*
+    GET - Creates and sends a template xlsx for uploading orders
+    */
+    spreadsheetTemplate: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        Merchant.findOne({_id: req.session.user})
+            .populate("inventory.ingredient")
+            .then((merchant)=>{
+                let workbook = xlsx.utils.book_new();
+                workbook.SheetNames.push("Order");
+                let workbookData = [];
+                let now = new Date().toISOString();
+
+                workbookData.push(["Name", "Date", "Taxes", "Fees", "Ingredients", "Quantity", "Price", "<- Price Per Unit"]);
+                workbookData.push([
+                    "<<Order Name>>",
+                    now.slice(0, 10),
+                    0,
+                    0,
+                    merchant.inventory[0].ingredient.name,
+                    0,
+                    0
+                ]);
+
+                for(let i = 1; i < merchant.inventory.length; i++){
+                    workbookData.push(["", "", "", "", merchant.inventory[i].ingredient.name, 0, 0]);
+                }
+
+                workbook.Sheets.Order = xlsx.utils.aoa_to_sheet(workbookData);
+                xlsx.writeFile(workbook, "SublineOrder.xlsx");
+                return res.download("SublineOrder.xlsx", (err)=>{
+                    fs.unlink("SublineOrder.xlsx", ()=>{});
+                });
+            })
+            .catch((err)=>{});
+    },
+
     /*
     DELETE - Remove an order from the database
     */
@@ -178,6 +341,12 @@ module.exports = {
                 return merchant.save();
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO REMOVE ORDER");
             });
     }

+ 2 - 4
controllers/otherData.js

@@ -1,10 +1,8 @@
+const Merchant = require("../models/merchant");
+
 const bcrypt = require("bcryptjs");
 const axios = require("axios");
 const path = require("path");
-const mailgun = require("mailgun-js")({apiKey: process.env.MG_SUBLINE_APIKEY, domain: "mail.thesubline.net"});
-const VerifyEmail = require("../emails/verifyEmail.js");
-
-const Merchant = require("../models/merchant");
 
 module.exports = {
     /*

+ 8 - 2
controllers/passwordReset.js

@@ -12,7 +12,7 @@ module.exports = {
     },
 
     generateCode: function(req, res){
-        Merchant.findOne({email: req.body.email})
+        Merchant.findOne({email: req.body.email.toLowerCase()})
             .then((merchant)=>{
                 if(merchant === null){
                     req.session.error = "USER WITH THIS EMAIL DOES NOT EXIST";
@@ -89,7 +89,13 @@ module.exports = {
                 }
             })
             .catch((err)=>{
-                req.session.error = "ERROR: UNABLE TO UPDATE YOUR PASSWORD AT THIS TIME";
+                if(typeof(err) === "string"){
+                    req.session.error = err;
+                }else if(err.name === "ValidationError"){
+                    req.session.error = err.errors[Object.keys(err.errors)[0]].properties.message;
+                }else{
+                    req.session.error = "ERROR: UNABLE TO UPDATE YOUR PASSWORD AT THIS TIME";
+                }
                 return res.redirect("/");
             });
     }

+ 201 - 28
controllers/recipeData.js

@@ -1,9 +1,12 @@
-const axios = require("axios");
-
 const Recipe = require("../models/recipe.js");
 const Merchant = require("../models/merchant.js");
 const ArchivedRecipe = require("../models/archivedRecipe.js");
-const Validator = require("./validator.js");
+
+const helper = require("./helper.js");
+
+const axios = require("axios");
+const xlsx = require("xlsx");
+const fs = require("fs");
 
 module.exports = {
     /*
@@ -24,11 +27,6 @@ module.exports = {
             return res.redirect("/");
         }
 
-        let validation = Validator.recipe(req.body);
-        if(validation !== true){
-            return res.json(validation);
-        }
-
         let recipe = new Recipe({
             merchant: req.session.user,
             name: req.body.name,
@@ -36,26 +34,26 @@ module.exports = {
             ingredients: req.body.ingredients
         });
 
-
-        Merchant.findOne({_id: req.session.user})
-            .then((merchant)=>{
-                merchant.recipes.push(recipe);
-                merchant.save()
-                    .catch((err)=>{
-                        return res.json("ERROR: UNABLE TO SAVE RECIPE");
-                    });
-            })
-            .catch((err)=>{
-                return res.json("ERROR: UNABLE TO RETRIEVE USER DATA");
-            });
-
         recipe.save()
             .then((newRecipe)=>{
                 return res.json(newRecipe);
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO SAVE INGREDIENT");
             });
+
+        Merchant.findOne({_id: req.session.user})
+            .then((merchant)=>{
+                merchant.recipes.push(recipe);
+                return merchant.save();
+            })
+            .catch((err)=>{});
     },
 
     /*
@@ -76,11 +74,6 @@ module.exports = {
             return res.redirect("/");
         }
 
-        let validation = Validator.recipe(req.body);
-        if(validation !== true){
-            return res.json(validation);
-        }
-
         Recipe.findOne({_id: req.body.id})
             .then((recipe)=>{
                 new ArchivedRecipe({
@@ -101,6 +94,12 @@ module.exports = {
                 res.json(recipe);
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO UPDATE RECIPE");
             });
     },
@@ -136,6 +135,12 @@ module.exports = {
                 return res.json({});
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO RETRIEVE USER DATA");
             });
     },
@@ -202,12 +207,17 @@ module.exports = {
                 return res.json({new: newRecipes, removed: deletedRecipes});
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO RETRIEVE MERCHANT DATA");
             });
     },
 
     updateRecipesSquare: function(req, res){
-        
         if(!req.session.user){
             req.session.error = "Must be logged in to do that";
             return res.redirect("/");
@@ -294,7 +304,170 @@ module.exports = {
                 return res.json({new: newRecipes, removed: merchantRecipes});
             })
             .catch((err)=>{
-                return "ERROR: UNABLE TO RETRIEVE RECIPE DATA FROM SQUARE";
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO RETRIEVE RECIPE DATA FROM SQUARE");
             });
+    },
+
+    createFromSpreadsheet: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        //read file, get the correct sheet, create array from sheet
+        let workbook = xlsx.readFile(req.file.path);
+        fs.unlink(req.file.path, ()=>{});
+
+        let sheets = Object.keys(workbook.Sheets);
+        let sheet = {};
+        for(let i = 0; i < sheets.length; i++){
+            let str = sheets[i].toLowerCase();
+            if(str === "recipe" || str === "recipes"){
+                sheet = workbook.Sheets[sheets[i]];
+            }
+        }
+
+        const array = xlsx.utils.sheet_to_json(sheet, {
+            header: 1
+        });
+
+        //get property locations
+        let locations = {};
+        for(let i = 0; i < array[0].length; i++){
+            switch(array[0][i].toLowerCase()){
+                case "name": locations.name = i; break;
+                case "price": locations.price = i; break;
+                case "ingredients": locations.ingredient = i; break;
+                case "ingredient amount": locations.amount = i; break;
+            }
+        }
+
+        let merchant = {};
+        let ingredients = [];
+        Merchant.findOne({_id: req.session.user})
+            .populate("inventory.ingredient")
+            .then((response)=>{
+                merchant = response;
+
+                for(let i = 0; i < merchant.inventory.length; i++){
+                    ingredients.push({
+                        id: merchant.inventory[i].ingredient._id,
+                        name: merchant.inventory[i].ingredient.name.toLowerCase(),
+                        unit: merchant.inventory[i].defaultUnit
+                    });
+                }
+
+                let recipes = [];
+                let currentRecipe = {};
+                for(let i = 1; i < array.length; i++){
+                    if(array[i].length === 0){
+                        continue;
+                    }
+
+                    if(array[i][locations.name] !== undefined){
+                        currentRecipe = {
+                            merchant: req.session.user,
+                            name: array[i][locations.name],
+                            price: parseInt(array[i][locations.price] * 100),
+                            ingredients: []
+                        }
+
+                        recipes.push(currentRecipe);
+                    }
+
+                    let exists = false;
+                    for(let j = 0; j < ingredients.length; j++){
+                        if(ingredients[j].name === array[i][locations.ingredient]){
+                            currentRecipe.ingredients.push({
+                                ingredient: ingredients[j].id,
+                                quantity: helper.convertQuantityToBaseUnit(array[i][locations.amount], ingredients[j].unit)
+                            });
+
+                            exists = true;
+                            break;
+                        }
+                    }
+
+                    if(exists === false){
+                        throw `CANNOT FIND INGREDIENT ${array[i][locations.ingredient]} FROM RECIPE ${array[i][locations.name]}`;
+                    }
+                }
+                
+                return Recipe.create(recipes);
+            })
+            .then((response)=>{
+                recipes = response;
+
+                for(let i = 0; i < recipes.length; i++){
+                    merchant.recipes.push(recipes[i]._id);
+                }
+
+                return merchant.save();
+            })
+            .then((merchant)=>{
+                return res.json(recipes);
+            })
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO CREATE YOUR INGREDIENTS");
+            });
+    },
+
+    spreadsheetTemplate: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+        
+        Merchant.findOne({_id: req.session.user})
+            .populate("inventory.ingredient")
+            .then((merchant)=>{
+                let workbook = xlsx.utils.book_new();
+                workbook.SheetNames.push("Recipes");
+                let workbookData = [];
+
+                workbookData.push(["Name", "Price", "Ingredients", "Ingredient Amount", "", "Ingredients Reference", "Ingredient Unit"]);
+                
+                for(let i = 0; i < merchant.inventory.length; i++){
+                    workbookData.push(["", "", "", "", "", merchant.inventory[i].ingredient.name, merchant.inventory[i].defaultUnit]);
+                }
+
+                if(workbookData.length < 5){
+                    for(let i = workbookData.length - 1; i < 5; i++){
+                        workbookData.push(["", "", "", ""]);
+                    }
+                }
+
+                workbookData[1][0] = "Example Recipe 1";
+                workbookData[1][1] = 10.98;
+                workbookData[1][2] = "Example Ingredient 1";
+                workbookData[1][3] = 1.2;
+                workbookData[2][2] = "Example Ingredient 2";
+                workbookData[2][3] =  0.55;
+                workbookData[3][0] = "Example Recipe 2";
+                workbookData[3][1] = 5.54;
+                workbookData[3][2] = "Example Ingredient 3";
+                workbookData[3][3] = 1;
+                workbookData[4][2] = "Example Ingredient 4";
+                workbookData[4][3] = 1.53; 
+
+                workbook.Sheets.Recipes = xlsx.utils.aoa_to_sheet(workbookData);
+                xlsx.writeFile(workbook, "SublineRecipes.xlsx");
+                return res.download("SublineRecipes.xlsx", (err)=>{
+                    fs.unlink("SublineRecipes.xlsx", ()=>{});
+                });
+            })
+            .catch((err)=>{});
     }
 }

+ 192 - 6
controllers/transactionData.js

@@ -4,6 +4,8 @@ const Merchant = require("../models/merchant");
 const helper = require("./helper.js");
 
 const ObjectId = require("mongoose").Types.ObjectId;
+const xlsx = require("xlsx");
+const fs = require("fs");
 
 module.exports = {
     /*
@@ -28,10 +30,11 @@ module.exports = {
         }
         let startDate = new Date(req.body.startDate);
         let endDate = new Date(req.body.endDate);
-        endDate = new Date(endDate.getFullYear(), endDate.getMonth(), endDate.getDate() + 1);
+        endDate.setDate(endDate.getDate() + 1);
+
         Transaction.aggregate([
             {$match: {
-                merchant: new ObjectId(req.session.user),
+                merchant: ObjectId(req.session.user),
                 date: {
                     $gte: startDate,
                     $lt: endDate
@@ -88,10 +91,155 @@ module.exports = {
                 return res.json(response);
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO CREATE NEW TRANSACTION");
             });
     },
 
+    createFromSpreadsheet: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        //read file, get the correct sheet, create array from sheet
+        let workbook = xlsx.readFile(req.file.path);
+        fs.unlink(req.file.path, ()=>{});
+
+        let sheets = Object.keys(workbook.Sheets);
+        let sheet = {};
+        for(let i = 0; i < sheets.length; i++){
+            let str = sheets[i].toLowerCase();
+            if(str === "transaction" || str === "transactions"){
+                sheet = workbook.Sheets[sheets[i]];
+            }
+        }
+
+        const array = xlsx.utils.sheet_to_json(sheet, {
+            header: 1
+        });
+
+        let locations = {};
+        for(let i = 0; i < array[0].length; i++){
+            if(array[0][i] === undefined){
+                continue;
+            }
+
+            switch(array[0][i].toLowerCase()){
+                case "date": locations.date = i; break;
+                case "recipes": locations.recipes = i; break;
+                case "quantity": locations.quantity = i; break;
+            }
+        }
+
+        Merchant.findOne({_id: req.session.user})
+            .populate("recipes")
+            .populate("inventory.ingredient")
+            .then((merchant)=>{
+                let transaction = new Transaction({
+                    merchant: req.session.user,
+                    recipes: []
+                });
+
+                if(array[1][0] === undefined){
+                    transaction.date = new Date();
+                }else{
+                    transaction.date = new Date(array[1][0]);
+                }
+                
+                let ingredients = [];
+                for(let i = 1; i < array.length; i++){
+                    if(array[i][locations.recipes] === undefined || array[i][locations.quantity === 0]){
+                        continue;
+                    }
+
+                    let exists = false;
+                    for(let j = 0; j < merchant.recipes.length; j++){
+                        if(merchant.recipes[j].name.toLowerCase() === array[i][locations.recipes].toLowerCase()){
+                            transaction.recipes.push({
+                                recipe: merchant.recipes[j],
+                                quantity: array[i][locations.quantity]
+                            });
+
+                            for(let k = 0; k < merchant.recipes[j].ingredients.length; k++){
+                                ingredients.push({
+                                    id: merchant.recipes[j].ingredients[k].ingredient,
+                                    quantity: array[i][locations.quantity] * merchant.recipes[j].ingredients[k].quantity
+                                });
+                            }
+
+                            exists = true;
+                            break;
+                        }
+                    }
+
+                    if(exists !== true){
+                        throw `COULD NOT FIND RECIPE ${array[i][locations.recipes]}`;
+                    }
+                }
+
+                for(let i = 0; i < ingredients.length; i++){
+                    for(let j = 0; j < merchant.inventory.length; j++){
+                        
+                        if(merchant.inventory[j].ingredient._id.toString() === ingredients[i].id.toString()){
+                            merchant.inventory[j].quantity -= ingredients[i].quantity;
+
+                            break;
+                        }
+                    }
+                }
+
+                return Promise.all([transaction.save(), merchant.save()]);
+            })
+            .then((response)=>{
+                return res.json(response[0]);
+            })
+            .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
+                return res.json("ERROR: UNABLE TO CREATE YOUR TRANSACTION");
+            });
+    },
+
+    spreadsheetTemplate: function(req, res){
+        if(!req.session.user){
+            req.session.error = "MUST BE LOGGED IN TO DO THAT";
+            return res.redirect("/");
+        }
+
+        Merchant.findOne({_id: req.session.user})
+            .populate("recipes")
+            .then((merchant)=>{
+                let workbook = xlsx.utils.book_new();
+                workbook.SheetNames.push("Transaction");
+                let workbookData = [];
+                let now = new Date().toISOString();
+
+                workbookData.push(["Date", "Recipes", "Quantity"]);
+                workbookData.push([now.slice(0, 10), merchant.recipes[0].name, 0]);
+
+                for(let i = 1; i < merchant.recipes.length; i++){
+                    workbookData.push(["", merchant.recipes[i].name, 0]);
+                }
+
+                workbook.Sheets.Transaction = xlsx.utils.aoa_to_sheet(workbookData);
+                xlsx.writeFile(workbook, "SublineTransaction.xlsx");
+                return res.download("SublineTransaction.xlsx", (err)=>{
+                    fs.unlink("SublineTransaction.xlsx", ()=>{});
+                });
+            })
+            .catch((err)=>{});
+    },
+
     /*
     DELETE - Remove a transaction from the database
     */
@@ -131,18 +279,35 @@ module.exports = {
                 return merchant.save();
             })
             .catch((err)=>{
+                if(typeof(err) === "string"){
+                    return res.json(err);
+                }
+                if(err.name === "ValidationError"){
+                    return res.json(err.errors[Object.keys(err.errors)[0]].properties.message);
+                }
                 return res.json("ERROR: UNABLE TO DELETE THE TRANSACTION");
             });
     },
 
+    /*
+    GET - get transactions between two dates, sorted and group by date
+    params:
+        from: Date string
+        to: Date string
+    return:
+        [{
+            date: Date
+            transactions:[[Recipe]]
+        }]
+    */
     getTransactionsByDate: function(req, res){
         if(!req.session.user){
             req.session.error = "MUST BE LOGGED IN TO DO THAT";
             return res.redirect("/");
         }
 
-        const from = new Date(req.body.from);
-        let to = new Date(req.body.to);
+        const from = new Date(req.params.from);
+        const to = new Date(req.params.to);
         to.setDate(to.getDate() + 1);
 
         Transaction.aggregate([
@@ -153,13 +318,34 @@ module.exports = {
                     $lt: to
                 }
             }},
-            {$sort: {date: 1}}
+            {$group: {
+                _id: {$function: {
+                    body: "function(year, month, date){return `${year}-${month}-${date}`;}",
+                    args: [{$year: "$date"}, {$month: "$date"}, {$dayOfMonth: "$date"}],
+                    lang: "js"
+                }},
+                transactions: {$push: {
+                    _id: "$_id",
+                    recipes: "$recipes"
+                }}
+            }},
+            {$project: {
+                _id: 0,
+                date: {$convert: {
+                    input: "$_id",
+                    to: "date"
+                }},
+                transactions: 1
+            }},
+            {$sort: {
+                date: 1
+            }}
         ])
             .then((transactions)=>{
                 return res.json(transactions);
             })
             .catch((err)=>{
-                return res.json("ERROR: UNABLE TO RETRIEVE YOUR DATA");
+                return res.json("ERROR: UNABLE TO RETRIEVE DATA");
             });
     },
 

+ 0 - 161
controllers/validator.js

@@ -1,161 +0,0 @@
-const Merchant = require("../models/merchant.js");
-
-module.exports = {
-    merchant: async function(merchant){
-        if(!this.isSanitary([merchant.name])){
-            return "NAME CONTAINS ILLEGAL CHARACTERS";
-        }
-
-        if(!/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(merchant.email)){
-            return "INVALID EMAIL ADDRESS";
-        }
-
-        let checkMerchant = await Merchant.findOne({email: merchant.email});
-
-        if(checkMerchant){
-            return "AN ACCOUNT WITH THAT EMAIL ADDRESS ALREADY EXISTS";
-        }
-
-        let checkPassword = this.password(merchant.password, merchant.confirmPassword);
-        if(this.password(checkPassword !== true)){
-            return checkPassword;
-        }
-
-        return true;
-    },
-
-    password: function(password, confirmPassword){
-        if(password.length < 10){
-            return "PASSWORD MUST CONTAIN AT LEAST 10 CHARACTERS";
-        }
-
-        if(password !== confirmPassword){
-            return "PASSWORDS DO NOT MATCH";
-        }
-
-        return true;
-    },
-
-    quantity: function(num){
-        if(isNaN(num) || num === ""){
-            return "QUANTITY MUST BE A NUMBER";
-        }
-
-        if(num < 0){
-            return "QUANTITY CANNOT BE A NEGATIVE NUMBER";
-        }
-
-        return true;
-    },
-
-    price: function(price){
-        if(price < 0){
-            return "PRICE CANNOT BE A NEGATIVE NUMBER";
-        }
-
-        if(isNaN(price) || price === ""){
-            return "PRICE MUST BE A NUMBER";
-        }
-
-        return true;
-    },
-
-    /*
-    ingredient = {
-        name: required,
-        category: required,
-        unit: required,
-        quantity: required,
-        specialUnit: optional,
-        unitSize: optional
-    }
-    */
-    ingredient: function(ingredient){
-        if(!this.isSanitary([ingredient.name, ingredient.category])){
-            return "INGREDIENT CONTAINS ILLEGAL CHARACTERS";
-        }
-
-        if(ingredient.specialUnit === "bottle"){
-            let quantityCheck = this.quantity(ingredient.unitSize);
-            if(quantityCheck !== true){
-                return "BOTTLE SIZE MUST BE A NON-NEGATIVE NUMBER";
-            }
-        }
-
-        return true;
-    },
-
-    recipe: function(recipe){
-        if(!this.isSanitary([recipe.name])){
-            return "INGREDIENT CONTAINS ILLEGAL CHARACTERS";
-        }
-
-        let priceCheck = this.price(recipe.price);
-        if(priceCheck !== true){
-            return priceCheck;
-        }
-
-        for(let i = 0; i < recipe.ingredients.length; i++){
-            let checkQuantity = this.quantity(recipe.ingredients[i].quantity);
-            if(checkQuantity !== true){
-                return checkQuantity;
-            }
-        }
-
-        for(let i = 0; i < recipe.ingredients.length; i++){
-            for(let j = i + 1; j < recipe.ingredients.length; j++){
-                if(recipe.ingredients[i].ingredient === recipe.ingredients[j].ingredient){
-                    return "RECIPE CANNOT CONTAIN DUPLICATE INGREDIENTS";
-                }
-            }
-        }
-
-        return true;
-    },
-
-    order: function(order){
-        if(!this.isSanitary([order.name])){
-            return "ORDER NAME CONTAINS ILLEGAL CHARACTERS";
-        }
-
-        if(new Date(order.date) > new Date()){
-            return "DATE CANNOT BE IN THE FUTURE";
-        }
-
-        if(this.quantity(order.taxes) !== true){
-            return "TAXES MUST BE A NON NEGATIVE NUMBER";
-        }
-
-        if(this.quantity(order.fees) !== true){
-            return "FEES MUST BE A NON NEGATIVE NUMBER";
-        }
-
-        for(let i = 0; i < order.ingredients; i++){
-            let quantityCheck = this.quantity(order.ingredients[i].quantity);
-            if(quantityCheck !== true){
-                return quantityCheck;
-            }
-
-            let priceCheck = this.price(order.ingredients[i].price);
-            if(priceCheck !== true){
-                return priceCheck;
-            }
-        }
-
-        return true;
-    },
-
-    isSanitary: function(strings){
-        let disallowed = ["\\", "<", ">", "$", "{", "}", "(", ")"];
-
-        for(let i = 0; i < strings.length; i++){
-            for(let j = 0; j < disallowed.length; j++){
-                if(strings[i].includes(disallowed[j])){
-                    return false;
-                }
-            }
-        }
-
-        return true;
-    }
-}

+ 2 - 4
emails/verifyEmail.js

@@ -8,10 +8,8 @@ module.exports = (data)=>{
             </header>
 
             <h1 style="text-align:center;">Email Verification for ${data.name}</h1>
-
-            <p>Please enter the following code on the previous page.  Or use the link below to reopen the page.</p>
-
-            <p>CODE: ${data.code}</p>
+            
+            <p>Use the following link to verify your email:</p>
 
             <p>${data.link}</p>
         </div>

+ 17 - 13
models/ingredient.js

@@ -1,28 +1,32 @@
+const isSanitary = require("../controllers/helper.js").isSanitary;
+
 const mongoose = require("mongoose");
 
 const IngredientSchema = new mongoose.Schema({
     name: {
         type: String,
-        minlength: 2,
-        required: true
+        minlength: [2, "INGREDIENT NAME MUST CONTAIN AT LEAST 2 CHARACTERS"],
+        required: [true, "INGREDIENT NAME IS REQUIRED"],
+        validate: {
+            validator: isSanitary,
+            message: "INGREDIENT NAME CONTAINS ILLEGAL CHARACTERS"
+        }
     },
     category: {
         type: String,
-        minlength: 3,
-        required: true
+        minlength: [2, "INGREDIENT CATEGORY MUST CONTAIN AT LEAST 2 CHARACTERS"],
+        required: [true, "INGREDIENT CATEGORY IS REQUIRED"],
+        validate: {
+            validator: isSanitary,
+            message: "INGREDIENT CATEGORY CONTAINS ILLEGAL CHARACTERS"
+        }
     },
     unitType: {
         type: String,
-        required: true
-    },
-    specialUnit: {
-        type: String,
-        required: false
+        required: [true, "UNIT TYPE IS REQUIRED"]
     },
-    unitSize:{
-        type: Number,
-        required: false
-    }
+    specialUnit: String,
+    unitSize: String
 });
 
 module.exports = mongoose.model("Ingredient", IngredientSchema);

+ 23 - 7
models/merchant.js

@@ -1,14 +1,31 @@
+const isSanitary = require("../controllers/helper.js").isSanitary;
+
 const mongoose = require("mongoose");
 
+let emailValid = (value)=>{
+    /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(value);
+}
+
 const MerchantSchema = new mongoose.Schema({
     name: {
         type: String,
-        required: true
+        required: [true, "MERCHANT NAME IS REQUIRED"],
+        validate: {
+            validator: isSanitary,
+            message: "NAME CONTAINS ILLEGAL CHARACTERS"
+        }
+    },
+    email: {
+        type: String,
+        required: [true, "EMAIL IS REQUIRED"],
+        validate: {
+            validator: emailValid,
+            message: "INVALID EMAIL ADDRESS"
+        }
     },
-    email: String,
     password: {
         type: String,
-        minlength: 15
+        required: [true, "MUST PROVIDE A PASSWORD"],
     },
     pos: {
         type: String,
@@ -30,16 +47,15 @@ const MerchantSchema = new mongoose.Schema({
         ingredient: {
             type: mongoose.Schema.Types.ObjectId,
             ref: "Ingredient",
-            required: true
+            required: [true, "MUST PROVIDE THE INGREDIENT"]
         },
         quantity: {
             type: Number,
-            required: true,
-            min: 0
+            required: [true, "INGREDIENT QUANTITY IS REQUIRED"]
         },
         defaultUnit: {
             type: String,
-            required: true
+            required: [true, "INGREDIENT UNIT IS REQUIRED"]
         }
     }],
     recipes: [{

+ 18 - 15
models/order.js

@@ -1,39 +1,42 @@
+const isSanitary = require("../controllers/helper.js").isSanitary;
+
 const mongoose = require("mongoose");
 
 const OrderSchema = new mongoose.Schema({
     merchant: {
         type: mongoose.Schema.Types.ObjectId,
         ref: "Merchant",
-        required: true
+        required: [true, "MUST PROVIDE THE MERCHANT"]
+    },
+    name: {
+        type: String,
+        minlength: [2, "ORDER NAME MUST CONTAIN AT LEAST 2 CHARACTERS"],
+        validate: {
+            validator: isSanitary,
+            message: "ORDER NAME CONTAINS ILLEGAL CHARACTERS"
+        }
     },
-    name: String,
     date: {
         type: Date,
         default: Date.now,
-        required: true
-    },
-    taxes: {
-        type: Number,
-        required: true
-    },
-    fees: {
-        type: Number,
-        required: true
+        required: [true, "MUST PROVIDE A DATE FOR THE ORDER"]
     },
+    taxes: Number,
+    fees: Number,
     ingredients: [{
         ingredient: {
             type: mongoose.Schema.Types.ObjectId,
             ref: "Ingredient",
-            required: true
+            required: [true, "MUST PROVIDE THE INGREDIENT"]
         },
         quantity: {
             type: Number,
-            required: true,
-            min: 0
+            required: [true, "MUST PROVIDE THE QUANTITY FOR EVERY INGREDIENT IN THE ORDER"],
+            min: [0, "INGREDIENT QUANTITY IN AN ORDER CANNOT BE LESS THAN 0"]
         },
         pricePerUnit: {
             type: Number,
-            min: 0
+            min: [0, "PRICE PER UNIT CANNOT BE A NEGATIVE NUMBER"]
         }
     }]
 });

+ 14 - 6
models/recipe.js

@@ -1,3 +1,5 @@
+const isSanitary = require("../controllers/helper.js").isSanitary;
+
 const mongoose = require("mongoose");
 
 const RecipeSchema = new mongoose.Schema({
@@ -5,26 +7,32 @@ const RecipeSchema = new mongoose.Schema({
     merchant: {
         type: mongoose.Schema.Types.ObjectId,
         ref: "Merchant",
-        required: true
+        required: [true, "MERCHANT IS REQUIRED"]
     },
     name: {
         type: String,
-        required: true,
+        minlength: [2, "RECIPE NAME MUST CONTAIN AT LEAST 2 CHARACTERS"],
+        validate: {
+            validator: isSanitary,
+            message: "RECIPE NAME CONTAINS ILLEGAL CHARACTERS"
+        },
+        required: true
     },
     price: {
         type: Number,
-        min: 0
+        min: [0, "PRICE OF RECIPE CANNOT BE A NEGATIVE NUMBER"],
+        required: [true, "RECIPE PRICE IS REQUIRED"]
     },
     ingredients: [{
         ingredient: {
             type: mongoose.Schema.Types.ObjectId,
             ref: "Ingredient",
-            required: [true, "Must provide ingredient"]
+            required: [true, "INGREDIENT IS REQUIRED"]
         },
         quantity: {
             type: Number,
-            min: [0, "Cannot have a negative quantity"],
-            required: [true, "Must provide a quantity"]
+            min: [0, "QUANTITY OF INGREDIENTS CANNOT BE A NEGATIVE NUMBER"],
+            required: [true, "MUST PROVED A QUANTITY FOR ALL INGREDIENTS"]
         }
     }]
 });

+ 3 - 3
models/transaction.js

@@ -4,11 +4,11 @@ const TransactionSchema = new mongoose.Schema({
     merchant: {
         type: mongoose.Schema.Types.ObjectId,
         ref: "Merchant",
-        required: [true, "Transaction must contain a reference to a merchant"]
+        required: [true, "MERCHANT IS REQUIRED FOR A TRANSACTION"]
     },
     date: {
         type: Date,
-        required: [true, "Must provide date and time transacted"]
+        required: [true, "DATE MUST BE PROVIDED FOR A TRANSACTION"]
     },
     device: String,
     recipes: [{
@@ -18,7 +18,7 @@ const TransactionSchema = new mongoose.Schema({
         },
         quantity: {
             type: Number,
-            min: [0, "Must be a positive number"]
+            min: [0, "RECIPE QUANTITIES MUST BE A POSITIVE NUMBER"]
         }
     }],
     posId: String

+ 202 - 11
package-lock.json

@@ -105,6 +105,15 @@
       "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
       "dev": true
     },
+    "adler-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz",
+      "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
+    },
     "agent-base": {
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
@@ -141,6 +150,11 @@
         "normalize-path": "^2.1.1"
       }
     },
+    "append-field": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
+      "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
+    },
     "arr-diff": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@@ -701,8 +715,7 @@
     "buffer-from": {
       "version": "1.1.1",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
-      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==",
-      "dev": true
+      "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
     },
     "buffer-xor": {
       "version": "1.0.3",
@@ -730,6 +743,38 @@
         "through2": "^2.0.0"
       }
     },
+    "busboy": {
+      "version": "0.2.14",
+      "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
+      "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
+      "requires": {
+        "dicer": "0.2.5",
+        "readable-stream": "1.1.x"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "bytes": {
       "version": "3.1.0",
       "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -769,6 +814,16 @@
         "estraverse": "^4.0.0"
       }
     },
+    "cfb": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz",
+      "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==",
+      "requires": {
+        "adler-32": "~1.2.0",
+        "crc-32": "~1.2.0",
+        "printj": "~1.1.2"
+      }
+    },
     "chalk": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
@@ -848,6 +903,22 @@
       "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
       "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
     },
+    "codepage": {
+      "version": "1.14.0",
+      "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz",
+      "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=",
+      "requires": {
+        "commander": "~2.14.1",
+        "exit-on-epipe": "~1.0.1"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.14.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz",
+          "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw=="
+        }
+      }
+    },
     "collection-visit": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz",
@@ -960,7 +1031,6 @@
       "version": "1.6.2",
       "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
       "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
-      "dev": true,
       "requires": {
         "buffer-from": "^1.0.0",
         "inherits": "^2.0.3",
@@ -1052,6 +1122,15 @@
       "integrity": "sha1-4zST+2hgqC9xWdgjeEP7+u/uWWI=",
       "dev": true
     },
+    "crc-32": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz",
+      "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==",
+      "requires": {
+        "exit-on-epipe": "~1.0.1",
+        "printj": "~1.1.0"
+      }
+    },
     "create-ecdh": {
       "version": "4.0.3",
       "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
@@ -1302,6 +1381,38 @@
         "minimist": "^1.1.1"
       }
     },
+    "dicer": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
+      "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
+      "requires": {
+        "readable-stream": "1.1.x",
+        "streamsearch": "0.1.2"
+      },
+      "dependencies": {
+        "isarray": {
+          "version": "0.0.1",
+          "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+          "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+        },
+        "readable-stream": {
+          "version": "1.1.14",
+          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+          "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+          "requires": {
+            "core-util-is": "~1.0.0",
+            "inherits": "~2.0.1",
+            "isarray": "0.0.1",
+            "string_decoder": "~0.10.x"
+          }
+        },
+        "string_decoder": {
+          "version": "0.10.31",
+          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+          "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+        }
+      }
+    },
     "diffie-hellman": {
       "version": "5.0.3",
       "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@@ -1671,6 +1782,11 @@
         "safe-buffer": "^5.1.1"
       }
     },
+    "exit-on-epipe": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz",
+      "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw=="
+    },
     "expand-brackets": {
       "version": "2.1.4",
       "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
@@ -1969,6 +2085,11 @@
       "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
       "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
     },
+    "frac": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
+      "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA=="
+    },
     "fragment-cache": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -2891,8 +3012,7 @@
     "minimist": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
-      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
-      "dev": true
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
     },
     "mixin-deep": {
       "version": "1.3.2",
@@ -2915,6 +3035,14 @@
         }
       }
     },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
     "mkdirp-classic": {
       "version": "0.5.3",
       "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
@@ -3024,6 +3152,21 @@
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
       "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
     },
+    "multer": {
+      "version": "1.4.2",
+      "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+      "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
+      "requires": {
+        "append-field": "^1.0.0",
+        "busboy": "^0.2.11",
+        "concat-stream": "^1.5.2",
+        "mkdirp": "^0.5.1",
+        "object-assign": "^4.1.1",
+        "on-finished": "^2.3.0",
+        "type-is": "^1.6.4",
+        "xtend": "^4.0.0"
+      }
+    },
     "multi-stage-sourcemap": {
       "version": "0.2.1",
       "resolved": "https://registry.npmjs.org/multi-stage-sourcemap/-/multi-stage-sourcemap-0.2.1.tgz",
@@ -3122,8 +3265,7 @@
     "object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
-      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
-      "dev": true
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
     },
     "object-copy": {
       "version": "0.1.0",
@@ -3426,6 +3568,11 @@
       "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=",
       "dev": true
     },
+    "printj": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz",
+      "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ=="
+    },
     "process": {
       "version": "0.11.10",
       "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -4079,6 +4226,14 @@
         "extend-shallow": "^3.0.0"
       }
     },
+    "ssf": {
+      "version": "0.11.2",
+      "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
+      "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
+      "requires": {
+        "frac": "~1.1.2"
+      }
+    },
     "static-extend": {
       "version": "0.1.2",
       "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
@@ -4182,6 +4337,11 @@
         "readable-stream": "^2.0.2"
       }
     },
+    "streamsearch": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+      "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
+    },
     "string.prototype.trimend": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz",
@@ -4459,8 +4619,7 @@
     "typedarray": {
       "version": "0.0.6",
       "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
-      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
-      "dev": true
+      "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
     },
     "uglifyify": {
       "version": "5.0.2",
@@ -4693,6 +4852,16 @@
       "resolved": "https://registry.npmjs.org/with-callback/-/with-callback-1.0.2.tgz",
       "integrity": "sha1-oJYpuakgAo1yFAT7Q1vc/1yRvCE="
     },
+    "wmf": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
+      "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw=="
+    },
+    "word": {
+      "version": "0.3.0",
+      "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
+      "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA=="
+    },
     "word-wrap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
@@ -4710,6 +4879,29 @@
       "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
       "dev": true
     },
+    "xlsx": {
+      "version": "0.16.8",
+      "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.16.8.tgz",
+      "integrity": "sha512-qWub4YCn0xLEGHI7WWhk6IJ73MDu7sPSJQImxN6/LiI8wsHi0hUhICEDbyqBT+jgFgORZxrii0HvhNSwBNAPoQ==",
+      "requires": {
+        "adler-32": "~1.2.0",
+        "cfb": "^1.1.4",
+        "codepage": "~1.14.0",
+        "commander": "~2.17.1",
+        "crc-32": "~1.2.0",
+        "exit-on-epipe": "~1.0.1",
+        "ssf": "~0.11.2",
+        "wmf": "~1.0.1",
+        "word": "~0.3.0"
+      },
+      "dependencies": {
+        "commander": {
+          "version": "2.17.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+          "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg=="
+        }
+      }
+    },
     "xregexp": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-2.0.0.tgz",
@@ -4718,8 +4910,7 @@
     "xtend": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
-      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
-      "dev": true
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
     },
     "yallist": {
       "version": "3.1.1",

+ 3 - 1
package.json

@@ -28,7 +28,9 @@
     "ejs": "^2.7.4",
     "express": "^4.17.1",
     "mailgun-js": "^0.22.0",
-    "mongoose": "^5.9.26"
+    "mongoose": "^5.9.26",
+    "multer": "^1.4.2",
+    "xlsx": "^0.16.8"
   },
   "devDependencies": {
     "browserify": "^16.5.1",

+ 15 - 5
routes.js

@@ -9,6 +9,9 @@ const informationPages = require("./controllers/informationPages.js");
 const emailVerification = require("./controllers/emailVerification.js");
 const passwordReset = require("./controllers/passwordReset.js");
 
+const multer = require("multer");
+const upload = multer({dest: "uploads/"});
+
 module.exports = function(app){
     //Render page
     app.get("/", renderer.landingPage);
@@ -19,15 +22,16 @@ module.exports = function(app){
     app.post("/merchant/create/none", merchantData.createMerchantNone);
     app.get("/merchant/create/clover", merchantData.createMerchantClover);
     app.get("/merchant/create/square", merchantData.createMerchantSquare);
-    app.put("/merchant/ingredients/update/:id/:unit", merchantData.ingredientDefaultUnit);
     app.put("/merchant/ingredients/update", merchantData.updateMerchantIngredient); //also updates some data in ingredients
     app.post("/merchant/password", merchantData.updatePassword);
 
     //Ingredients
-    app.get("/ingredients", ingredientData.getIngredients);
     app.post("/ingredients/create", ingredientData.createIngredient);  //also adds to merchant
     app.put("/ingredients/update", ingredientData.updateIngredient);
+    app.post("/ingredients/create/spreadsheet", upload.single("ingredients"), ingredientData.createFromSpreadsheet);
+    app.get("/ingredients/download/spreadsheet", ingredientData.spreadsheetTemplate);
     app.delete("/ingredients/remove/:id", ingredientData.removeIngredient);
+    
 
     //Recipes
     app.post("/recipe/create", recipeData.createRecipe);
@@ -35,18 +39,24 @@ module.exports = function(app){
     app.delete("/recipe/remove/:id", recipeData.removeRecipe);
     app.get("/recipe/update/clover", recipeData.updateRecipesClover);
     app.get("/recipe/update/square", recipeData.updateRecipesSquare);
+    app.post("/recipes/create/spreadsheet", upload.single("recipes"), recipeData.createFromSpreadsheet);
+    app.get("/recipes/download/spreadsheet", recipeData.spreadsheetTemplate);
 
     //Orders
     app.get("/order", orderData.getOrders);
     app.post("/order", orderData.orderFilter);
     app.post("/order/create", orderData.createOrder);
+    app.post("/orders/create/spreadsheet", upload.single("orders"), orderData.createFromSpreadsheet);
+    app.get("/orders/download/spreadsheet", orderData.spreadsheetTemplate);
     app.delete("/order/:id", orderData.removeOrder);
 
     //Transactions
     app.post("/transaction", transactionData.getTransactions);
     app.post("/transaction/create", transactionData.createTransaction);
+    app.post("/transactions/create/spreadsheet", upload.single("transactions"), transactionData.createFromSpreadsheet);
+    app.get("/transactions/download/spreadsheet", transactionData.spreadsheetTemplate);
     app.delete("/transaction/:id", transactionData.remove);
-    app.post("/transaction/retrieve", transactionData.getTransactionsByDate);
+    app.get("/transactions/:from/:to", transactionData.getTransactionsByDate);
     app.get("/populatesometransactions", transactionData.populate);
 
     //Other
@@ -65,8 +75,8 @@ module.exports = function(app){
 
     //Email verification
     app.get("/verify/email/:id", emailVerification.sendVerifyEmail);
-    app.get("/verify/:id", emailVerification.verifyPage);
-    app.post("/verify", emailVerification.verify);
+    app.post("/verify/resend", emailVerification.resendEmail);
+    app.get("/verify/:id/:code", emailVerification.verify);
 
     //Password reset
     app.get("/reset/email", passwordReset.enterEmail);

File diff suppressed because it is too large
+ 0 - 0
views/dashboardPage/bundle.js


+ 58 - 0
views/dashboardPage/dashboard.css

@@ -151,6 +151,28 @@ Multi-strand use classes
             font-weight: bold;
         }
 
+.fileUpload{
+    background: rgb(240, 252, 255);
+    padding: 5px;
+    border-radius: 5px;
+    font-size: 16px;
+}
+
+    .fileUpload::file-selector-button{
+        background: rgb(0, 27, 45);
+        border-radius: 5px;
+        color: white;
+        border: none;
+        cursor: pointer;
+        font-size: 18px;
+        margin: auto 0;
+    }
+
+        .fileUpload::file-selector-button:hover{
+            background: rgb(201, 201, 201);
+            color: black;
+        }
+
 /* 
 Home Strand 
 */
@@ -533,6 +555,42 @@ Transactions Strand
         margin-top: 50px;
     }
 
+/*
+Modal
+*/
+.modal{
+    flex-direction: column;
+    justify-content: center;
+    align-items: center;
+    position: fixed;
+    height: 100vh;
+    width: 100vw;
+    background: rgba(0, 27, 45, 0.75);
+}
+
+    .modalBox{
+        display: flex;
+        flex-direction: column;
+        align-items: flex-end;
+    }
+
+        .modalBox svg{
+            color: rgb(255, 99, 107);
+            cursor: pointer;
+        }
+
+        .modalSpreadsheetUpload{
+            flex-direction: column;
+            align-items: center;
+            justify-content: space-between;
+            margin-top: 0;
+            padding: 25px;
+        }
+
+            .modalSpreadsheetUpload > *{
+                margin-top: 15px;
+            }
+
 @media screen and (max-width: 1000px){
     body{
         flex-direction: column;

+ 15 - 2
views/dashboardPage/dashboard.ejs

@@ -231,7 +231,7 @@
                         <span class="slider"><p>INGREDIENTS</p><p>RECIPES</p></span>
                     </label>
 
-                    <div>
+                    <!-- <div>
                         <label>FROM:
                             <input id="analStartDate" type="date">
                         </label>
@@ -241,11 +241,23 @@
                         </label>
     
                         <button id="analDateBtn" class="button">SUBMIT</button>
+                    </div> -->
+
+                    <div class="dateRange">
+                        <h3>Date Range:</h3>
+                        <div class="dateRangeInput">
+                            <input id="analStartDate" type="date">
+                            <p> - </p>
+                            <input id="analEndDate" type="date">
+                        </div>
+
+                        <button id="analDateBtn" class="button-small">SUBMIT</button>
                     </div>
+                    
                 </div>
 
                 <div id="analIngredientContent" class="analContent">
-                    <ul id="itemsList" class="itemsList"></ul>
+                    <div id="analIngredientList" class="itemsList"></div>
 
                     <div class="analData">
                         <div id="itemUseGraph" class="card analCard"></div>
@@ -402,6 +414,7 @@
         </div>
 
         <% include ../shared/loader %>
+        <% include ./modal %>
 
         <script>
             let data = {

+ 55 - 22
views/dashboardPage/js/classes/Merchant.js

@@ -1,10 +1,9 @@
+const Order = require("./Order.js");
+const Recipe = require("./Recipe.js");
+const Transaction = require("./Transaction.js");
+
 class MerchantIngredient{
     constructor(ingredient, quantity){
-        if(quantity < 0){
-            banner.createError("QUANTITY CANNOT BE A NEGATIVE NUMBER");
-            return false;
-        }
-        
         this._quantity = quantity;
         this._ingredient = ingredient;
     }
@@ -123,7 +122,7 @@ class Merchant{
                 for(let k = 0; k < this._ingredients.length; k++){
                     if(ingredient.ingredient === this._ingredients[k].ingredient.id){
                         ingredients.push({
-                            ingredient: this._ingredients[k].ingredient,
+                            ingredient: this._ingredients[k].ingredient.id,
                             quantity: ingredient.quantity
                         });
                         break;
@@ -174,8 +173,31 @@ class Merchant{
         return this._ingredients;
     }
 
-    addIngredient(ingredient, quantity){
-        const merchantIngredient = new MerchantIngredient(ingredient, quantity);
+    /*
+    ingredient: {
+        _id: String,
+        name: String,
+        category: String,
+        unitType: String,
+        specialUnit: String || undefined,
+        unitSize: Number || undefined
+    }
+    quantity: Number
+    defaultUnit: String
+    */
+    addIngredient(ingredient, quantity, defaultUnit){
+        const createdIngredient = new this._modules.Ingredient(
+            ingredient._id,
+            ingredient.name,
+            ingredient.category,
+            ingredient.unitType,
+            defaultUnit,
+            this,
+            ingredient.specialUnit,
+            ingredient.unitSize
+        )
+
+        const merchantIngredient = new MerchantIngredient(createdIngredient, quantity);
         this._ingredients.push(merchantIngredient);
 
         this._modules.home.isPopulated = false;
@@ -194,18 +216,6 @@ class Merchant{
         this._modules.ingredients.isPopulated = false;
     }
 
-    updateIngredient(ingredient, quantity){
-        const index = this._ingredients.indexOf(ingredient);
-        if(index === undefined){
-            return false;
-        }
-
-        this._ingredients[index].quantity = quantity;
-
-        this._modules.home.isPopulated = false;
-        this._modules.ingredients.isPopulated = false;
-    }
-
     getIngredient(id){
         for(let i = 0; i < this._ingredients.length; i++){
             if(this._ingredients[i].ingredient.id === id){
@@ -218,7 +228,9 @@ class Merchant{
         return this._recipes;
     }
 
-    addRecipe(recipe){
+    addRecipe(id, name, price, ingredients){
+        let recipe = new Recipe(id, name, price, ingredients, this);
+
         this._recipes.push(recipe);
 
         this._modules.recipeBook.isPopulated = false;
@@ -272,6 +284,10 @@ class Merchant{
         this._modules.recipeBook.isPopulated = false;
     }
 
+    get transactions(){
+        return this._transactions;
+    }
+
     getTransactions(from = 0, to = new Date()){
         if(merchant._transactions.length <= 0){
             return [];
@@ -287,6 +303,13 @@ class Merchant{
     }
 
     addTransaction(transaction){
+        transaction = new Transaction(
+            transaction._id,
+            transaction.date,
+            transaction.recipes,
+            this
+        );
+
         this._transactions.push(transaction);
         this._transactions.sort((a, b)=>{
             if(a.date > b.date){
@@ -362,7 +385,17 @@ class Merchant{
         return this._orders;
     }
 
-    addOrder(order, isNew = false){
+    addOrder(data, isNew = false){
+        let order = new Order(
+            data._id,
+            data.name,
+            data.date,
+            data.taxes,
+            data.fees,
+            data.ingredients,
+            this
+        );
+
         this._orders.push(order);
 
         if(isNew){

+ 0 - 15
views/dashboardPage/js/classes/Order.js

@@ -121,9 +121,6 @@ parent = the merchant that it belongs to
 */
 class Order{
     constructor(id, name, date, taxes, fees, ingredients, parent){
-        if(!this.isSanitaryString(name)){
-            return false;
-        }
         if(taxes < 0){
             return false;
         }
@@ -196,18 +193,6 @@ class Order{
     getTotalCost(){
         return (this.getIngredientCost() + this.taxes + this.fees);
     }
-
-    isSanitaryString(str){
-        let disallowed = ["\\", "<", ">", "$", "{", "}", "(", ")"];
-
-        for(let i = 0; i < disallowed.length; i++){
-            if(str.includes(disallowed[i])){
-                return false;
-            }
-        }
-
-        return true;
-    }
 }
 
 module.exports = Order;

+ 2 - 1
views/dashboardPage/js/classes/Recipe.js

@@ -111,8 +111,9 @@ class Recipe{
         this._ingredients = [];
 
         for(let i = 0; i < ingredients.length; i++){
+            const ingredient = parent.getIngredient(ingredients[i].ingredient);
             const recipeIngredient = new RecipeIngredient(
-                ingredients[i].ingredient,
+                ingredient.ingredient,
                 ingredients[i].quantity
             );
 

+ 18 - 0
views/dashboardPage/js/classes/Transaction.js

@@ -64,6 +64,24 @@ class Transaction{
     get recipes(){
         return this._recipes;
     }
+
+    /*
+    Gets the quantity for a given ingredient
+    */
+    getIngredientQuantity(ingredient){
+        let quantity = 0;
+
+        for(let i = 0; i < this._recipes.length; i++){
+            const recipe = this._recipes[i].recipe;
+            for(let j = 0; j < recipe.ingredients.length; j++){
+                if(recipe.ingredients[j].ingredient === ingredient){
+                    quantity += recipe.ingredients[j].quantity;
+                }
+            }
+        }
+
+        return quantity;
+    }
 }
 
 module.exports = Transaction;

+ 48 - 2
views/dashboardPage/js/dashboard.js

@@ -178,8 +178,6 @@ controller = {
                     choosables[i].classList.remove("active");
                 }
             }
-
-            
         }
         sidebar.classList = "sidebarHide";
 
@@ -190,6 +188,54 @@ controller = {
         }
     },
 
+    openModal: function(str){
+        let modal = document.getElementById("modal");
+        modal.style.display = "flex";
+        document.getElementById("modalClose").addEventListener("click", this.closeModal);
+        let content = {};
+
+        switch(str){
+            case "ingredientSpreadsheet":
+                content = document.getElementById("modalSpreadsheetUpload");
+                content.style.display = "flex";
+                document.getElementById("modalSpreadsheetTitle").innerText = "ingredients";
+                document.getElementById("spreadsheetDownload").href = "/ingredients/download/spreadsheet";
+                content.onsubmit = newIngredient.submitSpreadsheet;
+                break;
+            case "recipeSpreadsheet":
+                content = document.getElementById("modalSpreadsheetUpload");
+                content.style.display = "flex";
+                document.getElementById("modalSpreadsheetTitle").innerText = "recipes";
+                document.getElementById("spreadsheetDownload").href = "/recipes/download/spreadsheet";
+                content.onsubmit = newRecipe.submitSpreadsheet;
+                break;
+            case "orderSpreadsheet":
+                content = document.getElementById("modalSpreadsheetUpload");
+                content.style.display = "flex";
+                document.getElementById("modalSpreadsheetTitle").innerText = "orders";
+                document.getElementById("spreadsheetDownload").href = "/orders/download/spreadsheet";
+                content.onsubmit = newOrder.submitSpreadsheet;
+                break;
+            case "transactionSpreadsheet":
+                content = document.getElementById("modalSpreadsheetUpload");
+                content.style.display = "flex";
+                document.getElementById("modalSpreadsheetTitle").innerText = "transactions";
+                document.getElementById("spreadsheetDownload").href = "/transactions/download/spreadsheet";
+                content.onsubmit = newTransaction.submitSpreadsheet;
+        }
+    },
+
+    closeModal: function(){
+        let modal = document.getElementById("modal");
+        let modalContent = document.getElementById("modalContent");
+
+        for(let i = 0; i < modalContent.children.length; i++){
+            modalContent.children[i].style.display = "none";
+        }
+
+        modal.style.display = "none";
+    },
+
     changeMenu: function(){
         let menu = document.querySelector(".menu");
         let buttons = document.querySelectorAll(".menuButton");

+ 3 - 6
views/dashboardPage/js/sidebars/editIngredient.js

@@ -83,7 +83,7 @@ let editIngredient = {
             data.quantity = quantity * unitSize;
             data.unitSize = unitSize;
         }else{
-            data.quantity = ingredient.convertToBase(quantity);
+            data.quantity = quantity;
         }
 
         //Get the measurement unit
@@ -110,12 +110,9 @@ let editIngredient = {
             if(typeof(response) === "string"){
                 banner.createError(response);
             }else{
-                ingredient.ingredient.name = response.ingredient.name;
-                ingredient.ingredient.category = response.ingredient.category;
-                ingredient.ingredient.unitSize = response.ingredient.unitSize;
-                ingredient.ingredient.unit = response.unit;
+                merchant.removeIngredient(merchant.getIngredient(response.ingredient._id));
+                merchant.addIngredient(response.ingredient, response.quantity, response.unit);
 
-                merchant.updateIngredient(ingredient, response.quantity);
                 controller.openStrand("ingredients");
                 banner.createNotification("INGREDIENT UPDATED");
             }

+ 37 - 14
views/dashboardPage/js/sidebars/newIngredient.js

@@ -10,6 +10,7 @@ let newIngredient = {
 
         selector.onchange = ()=>{this.unitChange()};
         document.getElementById("submitNewIng").onclick = ()=>{this.submit(Ingredient)};
+        document.getElementById("ingredientFileUpload").addEventListener("click", ()=>{controller.openModal("ingredientSpreadsheet")});
     },
 
     unitChange: function(){
@@ -22,7 +23,7 @@ let newIngredient = {
         }
     },
 
-    submit: function(Ingredient){
+    submit: function(){
         let unitSelector = document.getElementById("unitSelector");
         let options = document.querySelectorAll("#unitSelector option");
         const quantityValue = parseFloat(document.getElementById("newIngQuantity").value);
@@ -63,25 +64,13 @@ let newIngredient = {
                 if(typeof(response) === "string"){
                     banner.createError(response);
                 }else{
-                    const ingredient = new Ingredient(
-                        response.ingredient._id,
-                        response.ingredient.name,
-                        response.ingredient.category,
-                        response.ingredient.unitType,
-                        response.defaultUnit,
-                        merchant,
-                        response.ingredient.specialUnit,
-                        response.ingredient.unitSize
-                    )
-
-                    merchant.addIngredient(ingredient, response.quantity);
+                    merchant.addIngredient(response.ingredient, response.quantity, response.defaultUnit);
                     controller.openStrand("ingredients");
 
                     banner.createNotification("INGREDIENT CREATED");
                 }
             })
             .catch((err)=>{
-                console.log(err);
                 banner.createError("SOMETHING WENT WRONG. PLEASE REFRESH THE PAGE");
             })
             .finally(()=>{
@@ -89,7 +78,41 @@ let newIngredient = {
             });
     },
 
+    submitSpreadsheet: function(){
+        event.preventDefault();
+        controller.closeModal();
+
+        const file = document.getElementById("spreadsheetInput").files[0];
+        let data = new FormData();
+        data.append("ingredients", file);
+
+        let loader = document.getElementById("loaderContainer");
+        loader.style.display = "flex";
+
+        fetch("/ingredients/create/spreadsheet", {
+            method: "post",
+            body: data
+        })
+            .then(response => response.json())
+            .then((response)=>{
+                if(typeof(response) === "string"){
+                    banner.createError(response);
+                }else{
+                    for(let i = 0; i < response.length; i++){
+                        merchant.addIngredient(response[i].ingredient, response[i].quantity, response[i].defaultUnit);
 
+                        controller.openStrand("ingredients");
+                    }
+                }
+                
+            })
+            .catch((err)=>{
+                banner.createError("SOMETHING WENT WRONG.  TRY REFRESHING THE PAGE");
+            })
+            .finally(()=>{
+                loader.style.display = "none";
+            });
+    }
 }
 
 module.exports = newIngredient;

+ 40 - 11
views/dashboardPage/js/sidebars/newOrder.js

@@ -2,6 +2,7 @@ let newOrder = {
     display: function(Order){
         document.getElementById("sidebarDiv").classList.add("sidebarWide");
         document.getElementById("newOrderIngredientList").style.display = "flex";
+        document.getElementById("orderFileUpload").addEventListener("click", ()=>{controller.openModal("orderSpreadsheet")});
 
         let selectedList = document.getElementById("selectedIngredientList");
         while(selectedList.children.length > 0){
@@ -31,6 +32,7 @@ let newOrder = {
         div.ingredient = ingredient;
         div.children[0].children[1].onclick = ()=>{this.removeIngredient(div, element)};
 
+        //TODO: this should be handled by the class
         //Display units depending on the whether it is a special unit
         if(ingredient.ingredient.specialUnit === "bottle"){
             div.children[0].children[0].innerText = `${ingredient.ingredient.name} (BOTTLES)`;
@@ -108,17 +110,7 @@ let newOrder = {
                 if(typeof(response) === "string"){
                     banner.createError(response);
                 }else{
-                    let order = new Order(
-                        response._id,
-                        response.name,
-                        response.date,
-                        response.taxes,
-                        response.fees,
-                        response.ingredients,
-                        merchant
-                    );
-
-                    merchant.addOrder(order, true);
+                    merchant.addOrder(response, true);
                     
                     controller.openStrand("orders", merchant.orders);
                     banner.createNotification("NEW ORDER CREATED");
@@ -132,6 +124,7 @@ let newOrder = {
             });
     },
 
+    //TODO: Remove this function, it should be on the order
     convertPrice: function(ingredient, price){
         if(ingredient.specialUnit === "bottle"){
             return price / ingredient.unitSize;
@@ -157,6 +150,42 @@ let newOrder = {
             case "in": return price * 39.3701; 
             case "ft": return price * 3.2808; 
         }
+    },
+
+    submitSpreadsheet: function(){
+        event.preventDefault();
+        controller.closeModal();
+
+        const file = document.getElementById("spreadsheetInput").files[0];
+        let data = new FormData();
+        data.append("orders", file);
+
+        let loader = document.getElementById("loaderContainer");
+        loader.style.display = "flex";
+
+        fetch("/orders/create/spreadsheet", {
+            method: "post",
+            body: data
+        })
+            .then(response => response.json())
+            .then((response)=>{
+                if(typeof(response) === "string"){
+                    banner.createError(response);
+                }else{
+                    for(let i = 0; i < response.length; i++){
+                        merchant.addOrder(response[i], true);
+                    }
+
+                    banner.createNotification("ORDER CREATED AND INGREDIENTS UPDATED SUCCESSFULLY");
+                    controller.openStrand("orders");
+                }
+            })
+            .catch((err)=>{
+                banner.createError("UNABLE TO DISPLAY NEW ORDER. PLEASE REFRESH THE PAGE.");
+            })
+            .finally(()=>{
+                loader.style.display = "none";
+            });
     }
 }
 

+ 45 - 4
views/dashboardPage/js/sidebars/newRecipe.js

@@ -15,6 +15,7 @@ let newRecipe = {
 
         document.getElementById("ingredientCount").onchange = ()=>{this.changeIngredientCount(categories)};
         document.getElementById("submitNewRecipe").onclick = ()=>{this.submit(Recipe)};
+        document.getElementById("recipeFileUpload").onclick = ()=>{controller.openModal("recipeSpreadsheet")};
     },
 
     //Updates the number of ingredient inputs displayed for new recipes
@@ -108,13 +109,12 @@ let newRecipe = {
                         }
                     }
 
-                    merchant.addRecipe(new Recipe(
+                    merchant.addRecipe(
                         response._id,
                         response.name,
                         response.price,
-                        ingredients,
-                        merchant
-                    ));
+                        ingredients
+                    );
 
                     banner.createNotification("RECIPE CREATED");
                     controller.openStrand("recipeBook");
@@ -127,6 +127,47 @@ let newRecipe = {
                 loader.style.display = "none";
             });
     },
+
+    submitSpreadsheet: function(){
+        event.preventDefault();
+        controller.closeModal();
+
+        const file = document.getElementById("spreadsheetInput").files[0];
+        let data = new FormData();
+        data.append("recipes", file);
+
+        let loader = document.getElementById("loaderContainer");
+        loader.style.display = "flex";
+
+        fetch("/recipes/create/spreadsheet", {
+            method: "post",
+            body: data
+        })
+            .then(response => response.json())
+            .then((response)=>{
+                if(typeof(response) === "String"){
+                    banner.createError(response);
+                }else{
+                    for(let i = 0; i < response.length; i++){
+                        merchant.addRecipe(
+                            response[i]._id,
+                            response[i].name,
+                            response[i].price,
+                            response[i].ingredients
+                        );
+                    }
+
+                    banner.createNotification("ALL INGREDIENTS SUCCESSFULLY CREATED");
+                    controller.openStrand("recipeBook");
+                }
+            })
+            .catch((err)=>{
+                banner.createError("UNABLE TO DISPLAY NEW RECIPES.  PLEASE REFRESH THE PAGE");
+            })
+            .finally(()=>{
+                loader.style.display = "none";
+            });
+    }
 }
 
 module.exports = newRecipe;

+ 39 - 8
views/dashboardPage/js/sidebars/newTransaction.js

@@ -2,6 +2,7 @@ let newTransaction = {
     display: function(Transaction){
         let recipeList = document.getElementById("newTransactionRecipes");
         let template = document.getElementById("createTransaction").content.children[0];
+        document.getElementById("transactionFileUpload").addEventListener("click", ()=>{controller.openModal("transactionSpreadsheet")});
 
         while(recipeList.children.length > 0){
             recipeList.removeChild(recipeList.firstChild);
@@ -72,14 +73,7 @@ let newTransaction = {
                     if(typeof(response) === "string"){
                         banner.createError(response);
                     }else{
-                        const transaction = new Transaction(
-                            response._id,
-                            response.date,
-                            response.recipes,
-                            merchant
-                        );
-
-                        merchant.addTransaction(transaction);
+                        merchant.addTransaction(response);
 
                         controller.openStrand("transactions", merchant.getTransactions());
                         banner.createNotification("TRANSACTION CREATED");
@@ -92,6 +86,43 @@ let newTransaction = {
                     loader.style.display = "none";
                 });
         }
+    },
+
+    submitSpreadsheet: function(){
+        event.preventDefault();
+        controller.closeModal();
+
+        const file = document.getElementById("spreadsheetInput").files[0];
+        let data = new FormData();
+        data.append("transactions", file);
+
+        let loader = document.getElementById("loaderContainer");
+        loader.style.display = "flex";
+
+        fetch("/transactions/create/spreadsheet", {
+            method: "post",
+            body: data
+        })
+            .then(response => response.json())
+            .then((response)=>{
+                if(typeof(response) === "string"){
+                    banner.createError(response);
+                }else{
+                    for(let i = 0; i < response.recipes.length; i++){
+                        response.recipes[i].recipe = response.recipes[i].recipe._id;
+                    }
+                    merchant.addTransaction(response);
+
+                    controller.openStrand("transactions", merchant.transactions);
+                    banner.createNotification("TRANSACTION SUCCESSFULLY CREATED.  INGREDIENTS UPDATED");
+                }
+            })
+            .catch((err)=>{
+                banner.createError("UNABLE TO DISPLAY NEW TRANSACTIONS.  PLEASE REFRESH THE PAGE");
+            })
+            .finally(()=>{
+                loader.style.display = "none";
+            });
     }
 }
 

+ 1 - 1
views/dashboardPage/js/sidebars/orderDetails.js

@@ -26,7 +26,7 @@ let orderDetails = {
             
             let ingredientDisplay = ingredientDiv.children[1];
             if(ingredient.specialUnit === "bottle"){
-                ingredientDisplay.innerText = `${order.ingredients[i].quantity.toFixed(2)} bottles x $${order.ingredients.pricePerUnit.toFixed(2)}`;
+                ingredientDisplay.innerText = `${order.ingredients[i].quantity.toFixed(2)} bottles x $${order.ingredients[i].pricePerUnit.toFixed(2)}`;
             }else{
                 ingredientDisplay.innerText = `${order.ingredients[i].quantity.toFixed(2)} ${ingredient.unit.toUpperCase()} X $${order.ingredients[i].pricePerUnit.toFixed(2)}`;
             }

+ 170 - 225
views/dashboardPage/js/strands/analytics.js

@@ -1,153 +1,128 @@
 let analytics = {
-    newData: false,
-    dateChange: false,
-    transactions: [],
-    ingredient: {},
-    recipe: {},
+    isPopulated: false,
+    ingredient: undefined,
+    recipe: undefined,
+    transactionsByDate: [],
 
     display: function(Transaction){
-        document.getElementById("analDateBtn").onclick = ()=>{this.changeDates(Transaction)};
+        if(!this.isPopulated){
+            document.getElementById("analRecipeContent").style.display = "none";
 
-        if(this.transactions.length === 0 || this.newData === true){
-            let startDate = new Date();
-            startDate.setMonth(startDate.getMonth() - 1);
-            this.transactions = merchant.getTransactions(startDate);
-        }
+            let to = new Date()
+            let from = new Date(to.getFullYear(), to.getMonth() - 1, to.getDate());
 
-        let slider = document.getElementById("analSlider");
-        slider.onchange = ()=>{this.display(Transaction)};
+            document.getElementById("analStartDate").valueAsDate = from;
+            document.getElementById("analEndDate").valueAsDate = to;
+            let analSlider = document.getElementById("analSlider");
+            analSlider.onclick = ()=>{this.switchDisplay()};
+            analSlider.checked = false;
+            document.getElementById("analDateBtn").onclick = ()=>{this.newDates(Transaction)};
 
-        let ingredientContent = document.getElementById("analIngredientContent");
-        let recipeContent = document.getElementById("analRecipeContent");
+            this.populateButtons();
 
-        if(slider.checked){
-            ingredientContent.style.display = "none";
-            recipeContent.style.display = "flex";
-            this.displayRecipes();
-        }else{
-            ingredientContent.style.display = "flex";
-            recipeContent.style.display = "none"
-            this.displayIngredients();
+            let hasIngredients
+
+            if(merchant.ingredients.length > 0){
+                this.ingredient = merchant.ingredients[0].ingredient;
+            }
+            if(merchant.recipes.length > 0){
+                this.recipe = merchant.recipes[0];
+            }
+            
+            this.newDates(Transaction);
+            
+            this.isPopulated = true;
         }
     },
 
-    displayIngredients: function(){
-        const itemsList = document.getElementById("itemsList");
-
-        while(itemsList.children.length > 0){
-            itemsList.removeChild(itemsList.firstChild);
-        }
+    populateButtons: function(){
+        let ingredientButtons = document.getElementById("analIngredientList");
+        let recipeButtons = document.getElementById("analRecipeList");
 
         for(let i = 0; i < merchant.ingredients.length; i++){
-            let li = document.createElement("li");
-            li.classList.add("choosable");
-            li.item = merchant.ingredients[i];
-            li.innerText = merchant.ingredients[i].ingredient.name;
-            li.onclick = ()=>{
-                const itemsList = document.getElementById("itemsList");
-                for(let i = 0; i < itemsList.children.length; i++){
-                    itemsList.children[i].classList.remove("active");
-                }
-
-                li.classList.add("active");
-
-                this.ingredient = merchant.ingredients[i];
-                this.ingredientDisplay();
+            let button = document.createElement("button");
+            button.innerText = merchant.ingredients[i].ingredient.name;
+            button.classList.add("choosable");
+            button.onclick = ()=>{
+                this.ingredient = merchant.ingredients[i].ingredient;
+                this.displayIngredient()
             };
-            itemsList.appendChild(li);
+            ingredientButtons.appendChild(button);
         }
 
-        if(this.dateChange && Object.keys(this.ingredient).length !== 0){
-            this.ingredientDisplay();
+        for(let i = 0; i < merchant.recipes.length; i++){
+            let button = document.createElement("button");
+            button.innerText = merchant.recipes[i].name;
+            button.classList.add("choosable");
+            button.onclick = ()=>{
+                this.recipe = merchant.recipes[i];
+                this.displayRecipe()
+            };
+            recipeButtons.appendChild(button);
         }
-        this.dateChange = false;
     },
 
-    displayRecipes: function(){
-        let recipeList = document.getElementById("analRecipeList");
-        while(recipeList.children.length > 0){
-            recipeList.removeChild(recipeList.firstChild);
-        }
-
-        for(let i = 0; i < merchant.recipes.length; i++){
-            let li = document.createElement("li");
-            li.classList.add("choosable");
-            li.recipe = merchant.recipes[i];
-            li.innerText = merchant.recipes[i].name;
-            li.onclick = ()=>{
-                let recipeList = document.getElementById("analRecipeList");
-                for(let i = 0; i < recipeList.children.length; i++){
-                    recipeList.children[i].classList.remove("active");
-                }
-                li.classList.add("active");
+    getData: function(from, to, Transaction){
+        let loader = document.getElementById("loaderContainer");
+        loader.style.display = "flex";
 
-                this.recipe = merchant.recipes[i];
-                this.recipeDisplay();
-            }
+        return fetch(`/transactions/${from.toISOString()}/${to.toISOString()}`)
+            .then(response => response.json())
+            .then((response)=>{
+                if(typeof(response) === "string"){
+                    banner.createError(response);
+                }else{
+                    this.transactionsByDate = [];
 
-            recipeList.appendChild(li);
-        }
+                    for(let i = 0; i < response.length; i++){
+                        const date = new Date(response[i].date);
+                        let newDate = {
+                            date: date,
+                            transactions: []
+                        };
+
+                        for(let j = 0; j < response[i].transactions.length; j++){
+                            newDate.transactions.push(new Transaction(
+                                response[i].transactions[j]._id,
+                                date,
+                                response[i].transactions[j].recipes,
+                                merchant
+                            ));
+                        }
 
-        if(this.dateChange  && Object.keys(this.recipe).length !== 0){
-            this.recipeDisplay();
-        }
-        this.dateChange = false;
+                        this.transactionsByDate.push(newDate);
+                    }
+                }
+            })
+            .catch((err)=>{
+                banner.createError("UNABLE TO UPDATE THE PAGE");
+            })
+            .finally(()=>{
+                loader.style.display = "none";
+            });
     },
 
-    ingredientDisplay: function(){
-        //Get list of recipes that contain the ingredient
-        let containingRecipes = [];
-
-        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 === this.ingredient.ingredient){
-                    containingRecipes.push({
-                        recipe: merchant.recipes[i],
-                        quantity: merchant.recipes[i].ingredients[j].quantity
-                    });
-
-                    break;
-                }
-            }
+    displayIngredient: function(ingredient){
+        if(this.ingredient === undefined  || this.transactionsByDate.length === 0){
+            return;
         }
-
-        //Create Graph
-        let quantities = [];
+        //break down data into dates and quantities
         let dates = [];
-        let currentDate = (this.transactions.length > 0) ? this.transactions[0].date : undefined;
-        let currentQuantity = 0;
-
-        for(let i = 0; i < this.transactions.length; i++){
-            if(currentDate.getDate() !== this.transactions[i].date.getDate()){
-                quantities.push(currentQuantity);
-                dates.push(currentDate);
-                currentQuantity = 0;
-                currentDate = this.transactions[i].date;
-            }
-
-            for(let j = 0; j < this.transactions[i].recipes.length; j++){
-                for(let k = 0; k < containingRecipes.length; k++){
-                    if(this.transactions[i].recipes[j].recipe === containingRecipes[k].recipe){
-                        for(let l = 0; l < this.transactions[i].recipes[j].recipe.ingredients.length; l++){
-                            const transIngredient = this.transactions[i].recipes[j].recipe.ingredients[l];
-
-                            if(transIngredient.ingredient === this.ingredient.ingredient){
-
-                                currentQuantity += transIngredient.quantity * this.transactions[i].recipes[j].quantity;
+        let quantities = [];
 
-                                break;
-                            }
-                        }
-                    }
-                }
-            }
+        for(let i = 0; i < this.transactionsByDate.length; i++){
+            dates.push(this.transactionsByDate[i].date);
 
-            if(i === this.transactions.length - 1){
-                quantities.push(currentQuantity);
-                dates.push(currentDate);
+            let sum = 0;
+            for(let j = 0; j < this.transactionsByDate[i].transactions.length; j++){
+                let transactions = this.transactionsByDate[i].transactions[j];
+                sum += transactions.getIngredientQuantity(this.ingredient);
             }
+            
+            quantities.push(sum);
         }
 
+        //create and display the graph
         let trace = {
             x: dates,
             y: quantities,
@@ -158,33 +133,34 @@ let analytics = {
         }
 
         const layout = {
-            title: this.ingredient.ingredient.name.toUpperCase(),
-            xaxis: {
-                title: "DATE"
-            },
-            yaxis: {
-                title: `QUANTITY (${this.ingredient.ingredient.unit.toUpperCase()})`,
-            }
+            title: this.ingredient.name.toUpperCase(),
+            xaxis: {title: "DATE"},
+            yaxis: {title: `QUANTITY (${this.ingredient.unit.toUpperCase()})`}
         }
 
         Plotly.newPlot("itemUseGraph", [trace], layout);
 
-        //Create use cards
+        //Create min/max/avg
+        //Current ingredient is stored on the "analMinUse" element
+        let min = quantities[0];
+        let max = quantities[0];
         let sum = 0;
-        let max = 0;
-        let min = (quantities.length > 0) ? quantities[0] : 0;
         for(let i = 0; i < quantities.length; i++){
-            sum += quantities[i];
+            if(quantities[i] < min){
+                min = quantities[i];
+            }
             if(quantities[i] > max){
                 max = quantities[i];
-            }else if(quantities[i] < min){
-                min = quantities[i];
             }
+
+            sum += quantities[i];
         }
-        document.getElementById("analMinUse").innerText = `${min.toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analAvgUse").innerText = `${(sum / quantities.length).toFixed(2)} ${this.ingredient.ingredient.unit}`;        
-        document.getElementById("analMaxUse").innerText = `${max.toFixed(2)} ${this.ingredient.ingredient.unit}`;
 
+        document.getElementById("analMinUse").innerText = `${min.toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analAvgUse").innerText = `${(sum / quantities.length).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analMaxUse").innerText = `${max.toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+
+        //Create weekday averages
         let dayUse = [0, 0, 0, 0, 0, 0, 0];
         let dayCount = [0, 0, 0, 0, 0, 0, 0];
         for(let i = 0; i < quantities.length; i++){
@@ -192,129 +168,98 @@ let analytics = {
             dayCount[dates[i].getDay()]++;
         }
 
-        document.getElementById("analDayOne").innerText = `${(dayUse[0] / dayCount[0]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analDayTwo").innerText = `${(dayUse[1] / dayCount[1]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analDayThree").innerText = `${(dayUse[2] / dayCount[2]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analDayFour").innerText = `${(dayUse[3] / dayCount[3]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analDayFive").innerText = `${(dayUse[4] / dayCount[4]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analDaySix").innerText = `${(dayUse[5] / dayCount[5]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
-        document.getElementById("analDaySeven").innerText = `${(dayUse[6] / dayCount[6]).toFixed(2)} ${this.ingredient.ingredient.unit}`;
+        document.getElementById("analDayOne").innerText = `${(dayUse[0] / dayCount[0]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analDayTwo").innerText = `${(dayUse[1] / dayCount[1]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analDayThree").innerText = `${(dayUse[2] / dayCount[2]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analDayFour").innerText = `${(dayUse[3] / dayCount[3]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analDayFive").innerText = `${(dayUse[4] / dayCount[4]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analDaySix").innerText = `${(dayUse[5] / dayCount[5]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
+        document.getElementById("analDaySeven").innerText = `${(dayUse[6] / dayCount[6]).toFixed(2)} ${this.ingredient.unit.toUpperCase()}`;
     },
 
-    recipeDisplay: function(){
-        let quantities = [];
-        let dates = [];
-        let currentDate;
-        let quantity = 0;
-        if(this.transactions.length > 0){
-            currentDate = this.transactions[0].date;
+    displayRecipe: function(){
+        if(this.recipes === undefined || this.transactionsByDate.length === 0){
+            return;
         }
 
-        for(let i = 0; i < this.transactions.length; i++){
-            if(currentDate.getDate() !== this.transactions[i].date.getDate()){
-                quantities.push(quantity);
-                quantity = 0;
-                dates.push(currentDate);
-                currentDate = this.transactions[i].date;
-            }
+        //break down data into dates and quantities
+        let dates = [];
+        let quantities = [];
+
+        for(let i = 0; i < this.transactionsByDate.length; i++){
+            dates.push(this.transactionsByDate[i].date);
+            let sum = 0;
 
-            for(let j = 0; j < this.transactions[i].recipes.length; j++){
-                const recipe = this.transactions[i].recipes[j];
+            for(let j = 0; j < this.transactionsByDate[i].transactions.length; j++){
+                const transaction = this.transactionsByDate[i].transactions[j];
 
-                if(recipe.recipe === this.recipe){
-                    quantity += recipe.quantity;
+                for(let k = 0; k < transaction.recipes.length; k++){
+                    if(transaction.recipes[k].recipe === this.recipe){
+                        sum += transaction.recipes[k].quantity;
+                    }
                 }
             }
 
-            if(i === this.transactions.length - 1){
-                quantities.push(quantity);
-                dates.push(currentDate);
-            }
+            quantities.push(sum);
         }
-
+        
+        //create and display the graph
         const trace = {
             x: dates,
             y: quantities,
             mode: "lines+markers",
             line: {
-                color: "rgb(255, 99, 107"
+                color: "rgb(255, 99, 107)"
             }
         }
 
         const layout = {
             title: this.recipe.name.toUpperCase(),
-            xaxis: {
-                title: "DATE"
-            },
-            yaxis: {
-                title: "Quantity"
-            }
+            xaxis: {title: "DATE"},
+            yaxis: {title: "QUANTITY"}
         }
 
         Plotly.newPlot("recipeSalesGraph", [trace], layout);
 
-        let sum = 0;
+        //Display the boxes at the bottom
+        //Current recipe is stored on the "recipeAvgUse" element
+        let avg = 0;
         for(let i = 0; i < quantities.length; i++){
-            sum += quantities[i];
+            avg += quantities[i];
         }
+        avg = avg / quantities.length;
 
-        document.getElementById("recipeAvgUse").innerText = (sum / quantities.length).toFixed(2);
-        document.getElementById("recipeAvgRevenue").innerText = `$${(((sum / quantities.length) * this.recipe.price) / 100).toFixed(2)}`;
+        document.getElementById("recipeAvgUse").innerText = avg.toFixed(2);
+        document.getElementById("recipeAvgRevenue").innerText = `$${(avg * this.recipe.price).toFixed(2)}`;
     },
 
-    changeDates: function(Transaction){
-        let dates = {
-            from: document.getElementById("analStartDate").valueAsDate,
-            to: document.getElementById("analEndDate").valueAsDate
-        }
+    switchDisplay: function(){
+        const checkbox = document.getElementById("analSlider");
+        let ingredient = document.getElementById("analIngredientContent");
+        let recipe = document.getElementById("analRecipeContent");
 
-        if(dates.from > dates.to || dates.from === "" || dates.to === "" || dates.to > new Date()){
-            banner.createError("INVALID DATE");
-            return;
+        if(checkbox.checked === true){
+            ingredient.style.display = "none";
+            recipe.style.display = "flex";
+            this.displayRecipe();
+        }else{
+            ingredient.style.display = "flex";
+            recipe.style.display = "none";
+            this.displayIngredient();
         }
+    },
 
-        let loader = document.getElementById("loaderContainer");
-        loader.style.display = "flex";
-
-        fetch("/transaction/retrieve", {
-            method: "post",
-            headers: {
-                "Content-Type": "application/json;charset=utf-8"
-            },
-            body: JSON.stringify(dates)
-        })
-            .then(response => response.json())
-            .then((response)=>{
-                if(typeof(response) === "string"){
-                    banner.createError(response.data);
-                }else{
-                    this.transactions = [];
+    newDates: async function(Transaction){
+        const from = document.getElementById("analStartDate").valueAsDate;
+        const to = document.getElementById("analEndDate").valueAsDate;
 
-                    for(let i = 0; i < response.length; i++){
-                        this.transactions.push(new Transaction(
-                            response[i]._id,
-                            new Date(response[i].date),
-                            response[i].recipes,
-                            merchant
-                        ));
-                    }
+        await this.getData(from, to, Transaction);
 
-                    let isRecipe = document.getElementById("analSlider").checked;
-                    if(isRecipe && Object.keys(this.recipe).length !== 0){
-                        this.recipeDisplay();
-                    }else if(!isRecipe && Object.keys(this.ingredient).length !== 0){
-                        this.ingredientDisplay();
-                    }
-                    
-                    this.dateChange = true;
-                }
-            })
-            .catch((err)=>{
-                banner.createError("ERROR: UNABLE TO DISPLAY THE DATA");
-            })
-            .finally(()=>{
-                loader.style.display = "none";
-            });
+        if(document.getElementById("analSlider").checked === true){
+            this.displayRecipe();
+        }else{
+            this.displayIngredient();
+        }
     }
 }
 

+ 12 - 24
views/dashboardPage/js/strands/home.js

@@ -211,30 +211,15 @@ let home = {
     submitInventoryCheck: function(){
         let lis = document.querySelectorAll("#inventoryCheckCard li");
 
-        let changes = [];
-        let fetchData = [];
+        let data = [];
 
         for(let i = 0; i < lis.length; i++){
             if(lis[i].children[1].children[1].value >= 0){
-                let merchIngredient = lis[i].ingredient;
-
                 if(lis[i].children[1].children[1].changed === true){
-                    let value = 0;
-                    if(merchIngredient.ingredient.specialUnit === "bottle"){
-                        value = parseFloat(lis[i].children[1].children[1].value) * merchIngredient.ingredient.unitSize;
-                    }else{
-                        value = controller.convertToMain(merchIngredient.ingredient.unit, parseFloat(lis[i].children[1].children[1].value));
-                    }
-                    
-
-                    changes.push({
-                        ingredient: merchIngredient.ingredient,
-                        quantity: value
-                    });
-
-                    fetchData.push({
+                    let merchIngredient = lis[i].ingredient;
+                    data.push({
                         id: merchIngredient.ingredient.id,
-                        quantity: value
+                        quantity: lis[i].children[1].children[1].value
                     });
 
                     lis[i].children[1].children[1].changed = false;
@@ -245,7 +230,7 @@ let home = {
             }
         }
         
-        if(fetchData.length > 0){
+        if(data.length > 0){
             let loader = document.getElementById("loaderContainer");
             loader.style.display = "flex";
 
@@ -254,20 +239,23 @@ let home = {
                 headers: {
                     "Content-Type": "application/json;charset=utf-8"
                 },
-                body: JSON.stringify(fetchData)
+                body: JSON.stringify(data)
             })
                 .then(response => response.json())
                 .then((response)=>{
                     if(typeof(response) === "string"){
                         banner.createError(response);
                     }else{
-                        for(let i = 0; i < changes.length; i++){
-                            merchant.updateIngredient(changes[i].ingredient, changes[i].quantity);
+                        for(let i = 0; i < response.length; i++){
+                            merchant.removeIngredient(merchant.getIngredient(response[i].ingredient._id));
+                            merchant.addIngredient(response[i].ingredient, response[i].quantity, response[i].defaultUnit);
                         }
                         banner.createNotification("INGREDIENTS UPDATED");
                     }
                 })
-                .catch((err)=>{})
+                .catch((err)=>{
+                    banner.createError("SOMETHING WENT WRONG.  PLEASE REFRESH THE PAGE");
+                })
                 .finally(()=>{
                     loader.style.display = "none";
                 });

+ 7 - 8
views/dashboardPage/js/strands/orders.js

@@ -1,5 +1,4 @@
 let orders = {
-    orders: [],
 
     display: function(){
         document.getElementById("orderFilterBtn").onclick = ()=>{controller.openSidebar("orderFilter")};
@@ -12,15 +11,15 @@ let orders = {
             orderList.removeChild(orderList.firstChild);
         }
 
-        for(let i = 0; i < this.orders.length; i++){
+        for(let i = 0; i < merchant.orders.length; i++){
             let orderDiv = template.cloneNode(true);
-            orderDiv.order = this.orders[i];
-            orderDiv.children[0].innerText = this.orders[i].name;
-            orderDiv.children[1].innerText = `${this.orders[i].ingredients.length} ingredients`;
-            orderDiv.children[2].innerText = this.orders[i].date.toLocaleDateString("en-US");
-            orderDiv.children[3].innerText = `$${this.orders[i].getTotalCost().toFixed(2)}`;
+            orderDiv.order = merchant.orders[i];
+            orderDiv.children[0].innerText = merchant.orders[i].name;
+            orderDiv.children[1].innerText = `${merchant.orders[i].ingredients.length} ingredients`;
+            orderDiv.children[2].innerText = merchant.orders[i].date.toLocaleDateString("en-US");
+            orderDiv.children[3].innerText = `$${merchant.orders[i].getTotalCost().toFixed(2)}`;
             orderDiv.onclick = ()=>{
-                controller.openSidebar("orderDetails", this.orders[i]);
+                controller.openSidebar("orderDetails", merchant.orders[i]);
                 orderDiv.classList.add("active");
             }
             orderList.appendChild(orderDiv);

+ 18 - 0
views/dashboardPage/modal.ejs

@@ -0,0 +1,18 @@
+<div id="modal" class="modal" style="display:none;">
+    <div class="modalBox">
+        <svg id="modalClose" width="35" height="35" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+            <line x1="18" y1="6" x2="6" y2="18"></line>
+            <line x1="6" y1="6" x2="18" y2="18"></line>
+        </svg>
+
+        <div id="modalContent">
+            <form id="modalSpreadsheetUpload" class="modalSpreadsheetUpload" method="post">
+                <h3>Choose a file to upload your <span id="modalSpreadsheetTitle"></span></h3>
+                <input id="spreadsheetInput" class="fileUpload" name="file" type="file">
+                <input type="hidden" name="type">
+                <input class="button" type="submit" value="SUBMIT">
+                <a id="spreadsheetDownload">download template</a>
+            </form>
+        </div>
+    </div>
+</div>

+ 5 - 4
views/dashboardPage/sidebars.css

@@ -377,10 +377,6 @@ Add Recipe
                 color: rgb(255, 99, 107);
             }
 
-    #addRecipe button:last-of-type{
-        margin-top: auto;
-    }
-
 /*
 EDIT RECIPE
 */
@@ -534,6 +530,11 @@ EDIT RECIPE
         margin: 15px auto;
     }
 
+    #newIngredient h2{
+        text-align: center;
+        margin-bottom: 25px;
+    }
+
 /* Order Details */
 #orderDetails{
     display: flex;

+ 5 - 1
views/dashboardPage/sidebars/newIngredient.ejs

@@ -8,7 +8,7 @@
         </button>
     </div>
 
-    <h1>NEW INGREDIENT</h1>
+    <h1>CREATE INGREDIENT</h1>
 
     <label>NAME:
         <input id="newIngName" type="text">
@@ -74,4 +74,8 @@
     </label>
 
     <button id="submitNewIng" class="button">CREATE</button>
+
+    <div class="lineBorder"></div>
+
+    <button id="ingredientFileUpload" class="linkButton">Or, upload a spreadsheet</button>
 </div>

+ 2 - 0
views/dashboardPage/sidebars/newOrder.ejs

@@ -38,6 +38,8 @@
 
     <button id="submitNewOrder" class="button">CREATE</button>
 
+    <button id="orderFileUpload" class="linkButton">Or, upload a spreadsheet</button>
+
     <template id="selectedIngredient">
         <div class="selectedIngredient">
             <div>

+ 2 - 0
views/dashboardPage/sidebars/newRecipe.ejs

@@ -43,4 +43,6 @@
     </template>
     
     <button id="submitNewRecipe" class="button">CREATE</button>
+
+    <button id="recipeFileUpload" class="linkButton">Or, upload a spreadsheet</button>
 </div>

+ 2 - 0
views/dashboardPage/sidebars/newTransaction.ejs

@@ -16,6 +16,8 @@
 
     <button id="submitNewTransaction" class="button">Create</button>
 
+    <button id="transactionFileUpload" class="linkButton">Or, upload a spreadsheet</button>
+
     <template id="createTransaction">
         <div class="newTransactionRecipe">
             <p></p>

+ 34 - 6
views/informationPages/help.ejs

@@ -19,12 +19,12 @@
                 <li><a href="#dashboard">Dashboard</a></li>
                 <li><a href="#ingredients">Ingredients</a>
                     <ul class="indent">
+                        <li><a href="#ingredientsDetails">Ingredient Details</a></li>
                         <li><a href="#ingredientsNew">New Ingredients</a>
                             <ul class="indent">
                                 <li><a href="#ingredientsNewBottles">Bottle Unit</a></li>
                             </ul>
                         </li>
-                        <li><a href="#ingredientsDetails">Ingredient Details</a></li>
                     </ul>
                 </li>
                 <li><a href="#recipeBook">Recipe Book</a>
@@ -51,6 +51,14 @@
                         <li><a href="#transactionsNew">New Transactions</a></li>
                     </ul>
                 </li>
+                <li><a href="#spreadsheets">Uploading Spreadsheets</a>
+                    <ul class="indent">
+                        <li><a href="#ingredientsSpreadsheet">Ingredients Spreadsheet</a></li>
+                        <li><a href="#recipesSpreadsheet">Recipes Spreadsheet</a></li>
+                        <li><a href="#orderSpreadsheet">Order Spreadsheet</a></li>
+                        <li><a href="#transactionSpreadsheet">Transaction Spreadsheet</a></li>
+                    </ul>
+                </li>
                 <li><a href="#contact">Contact Us</a></li>
             </ul>
         </div>
@@ -89,17 +97,17 @@
 
             <p>Near the top of the page and on the right is a "sort by" option.  If you click on this then a dropdown will appear with several options.  The "Ingredient" option will display an uncategorized list of all ingredients in alphabetical order.  The "Category" option will show a categorized list, which is the default display.  The "Unit of Measurement" option will categorize your ingredients based on their measurement units, similar to "Category".  Click on the red "X" at any time to revert to the default.</p>
 
+            <h3 id="ingredientsDetails">INGREDIENT DETAILS</h3>
+            <p>You can click on any of the ingredients to get more details on it.  After clicking on an ingredient, a sidebar should appear to the right.  At the top of this is the category and name of the ingredient.  Next is the current stock that you should have for this item.  After that is the average daily use for the past 30 days.  As the name suggests, this show how much you use this ingredient every day, on average.  Last is the recipes.  This simply shows all of your recipes that contain this ingredient.  You can click on the recipes to get further information on that recipe.  We discuss recipes further down.</p>
+
+            <p>At the top of the sidebar are three icons.  The arrow on the left will close the sidebar.  The trash can on the right will delete the ingredient.  In order to delete an ingredient, it must first be removed from all recipes that contain the ingredient.  Be careful, this cannot be undone.  The pencil icon in the center will allow you to edit the ingredient.  You can update the name, category and current stock of the current ingredient.  Also, you can change the units that want to use.  So, if you use pounds for an ingredient, but decide to switch over to metric, then your can change it to kilograms.  Or the other way around.  Click save to make the changes, or cancel to cancel them.</p>
+
             <h3 id="ingredientsNew">NEW INGREDIENTS</h3>
             <p>There is a button in the top right of the ingredients page labeled "NEW".  Clicking on this will open a sidebar on the right that allows you to create a new ingredient.  There are 4 fields that you need to fill out in order to create a new ingredient.  The name is whatever you want to call the ingredient and the category can be used to group it together with other similar ingredients.  The quantity is the number amount that you have in stock of that particular ingredient.  Match this with a unit in the next field.  When choosing a unit, keep in mind that you will be able to freely change the unit in the future.  However, you can only change units with the same type.  For example, you can change LB (mass), to KG (mass), but you cannot change LB (mass) to GAL(volume).</p>
 
             <h4 id="ingredientsNewBottles">BOTTLE UNIT</h4>
             <p>One of the special type of ingredients that can choose is the bottle type.  Bottles, obviously, are for things that purchase as bottles (i.e. liquour, wine) but tend to sell as smaller units(i.e. shots, glasses).  If you choose the bottle option, then another field will appear asking for bottle size.  Here you need to choose the unit (volume) that the bottle is measured in and then the quantity of each bottle.  The unit for the bottle can be changed at any time.  When creating recipes for bottle items, which is discussed further down, you will add in the volume that the recipe has and it will update the amount of the ingredient in bottles automatically</p>
 
-            <h3 id="ingredientsDetails">INGREDIENT DETAILS</h3>
-            <p>You can click on any of the ingredients to get more details on it.  After clicking on an ingredient, a sidebar should appear to the right.  At the top of this is the category and name of the ingredient.  Next is the current stock that you should have for this item.  After that is the average daily use for the past 30 days.  As the name suggests, this show how much you use this ingredient every day, on average.  Last is the recipes.  This simply shows all of your recipes that contain this ingredient.  You can click on the recipes to get further information on that recipe.  We discuss recipes further down.</p>
-
-            <p>At the top of the sidebar are three icons.  The arrow on the left will close the sidebar.  The trash can on the right will delete the ingredient.  In order to delete an ingredient, it must first be removed from all recipes that contain the ingredient.  Be careful, this cannot be undone.  The pencil icon in the center will allow you to edit the ingredient.  You can update the name, category and current stock of the current ingredient.  Also, you can change the units that want to use.  So, if you use pounds for an ingredient, but decide to switch over to metric, then your can change it to kilograms.  Or the other way around.  Click save to make the changes, or cancel to cancel them.</p>
-
             <h2 id="recipeBook">RECIPE BOOK</h2>
             <p>The third option on the menu is the "RECIPE BOOK".  Here you will find all of the information that you need about all of your recipes.  The recipes are displayed individual with the price of that recipe.</p>
 
@@ -174,6 +182,26 @@
 
             <p>When finished entering in all information, click the "CREATE" button at the bottom of the sidebar.  This will add your new transaction.  Also, all of your ingredients will be automatically updated based on the quantity of recipes sold, and the quantity of each ingredient within the recipe.  You can return to the ingredients page after creating a new transaction to find that the quantities of your ingredients have already been updated.</p>
 
+            <h2 id="spreadsheets">UPLOADING SPREADSHEETS</h2>
+            <p>In order to make the process of entering your data simpler, you have the option to use a spreadsheet to upload your data to the site.  This can be especially helpful if you need to create a large amount of data at once, such as for the initial uploading of ingredients or recipes.  The ability to upload data from a spreadsheet is available for ingredients, recipes, orders and transactions.  You will find a link to do this in the corresponding sidebar for creating new items, it should be located at the bottom of the sidebar.  Clicking on this link will open a window that will allow you to choose a file to submit, or you can download a template that can help you enter the data correctly.  All of the data must be carefully formatted, so it is recommended to use the template.  See below for instructions for each spreadsheet.</p>
+
+            <h3 id="ingredientsSpreadsheet">INGREDIENTS SPREADSHEET</h3>
+            <p>The first row of the ingredients spreadsheet is the header and must contain six column headers: name, category, quantity, unit, bottle, bottle size.  They may be in any order and capitalization does not matter.  From there, fill in the necessary data.  If the unit is a bottle, then enter TRUE in that column and the size of the bottle (based on the unit) in the "bottle size" column.  Otherwise leave these two columns blank.</p>
+
+            <h3 id="recipesSpreadsheet">RECIPES SPREADSHEET</h3>
+            <p>The first row of the recipes spreadsheet is the header and must contain four column headers: name, price, ingredients and ingredient amount.  The headers may be in any order and capitalization does not matter.  The name of the ingredient need only be filled in on the first row of the ingredient, then put each ingredient of the recipe in a seperate row, along with the quantity of that ingredient within the recipe.  It is recommended to download the template for an example.  If you download the template, then you will have all of your ingredients listed in column F, and their corresponding unit in column G.  Ingredient names must be entered exactly as they are listed (capitalization does not matter) in order to match them to your current ingredients.  You will be given an error telling you which ingredient failed to match if there is a mistake in the spreadsheet.  The ingredient amount is assumed to be in the same unit as the ingredient itself.</p>
+
+            <h3 id="orderSpreadsheet">ORDER SPREADSHEET</h3>
+            <p>It is strongly recommended that you download the template spreadsheet for orders.  This spreadsheet will contain seven headers: name, date, taxes, fees, ingredients, quantity and price.  Change the name(A2) to anything that you would like, such as the order id.  The date will be autofilled with the current date, but you may change it if you wish.  Be sure to enter the date in the same format (yyyy-mm-dd).  Taxes and fees are defaulted to 0, but may be changed.  The ingredients column contains a list of all of your ingredients, do not change anything in this column.  The quantity column(F) and the price column(G) are where you input the information for your order.  For any ingredients that you did not purchase in this particular order, you can lave it as 0.</p>
+
+            <p>NOTE: Price is price per measurement unit.  If the item is measured in lbs, then you need to enter the price per pound.</p>
+            <p>NOTE: Each spreadsheet may only contain a single order.  If you need to enter more than one order, then you must change the data and upload the file again.</p>
+
+            <h3 id="transactionSpreadsheet">TRANSACTION SPREADSHEET</h3>
+            <p>It is strongly recommended that you download the template spreadsheet for transactions.  This spreadsheet will contain three headers: date, recipes and quantity.  The date will be autofilled with the current date, but you may change it if you wish.  Be sure to enter the date in the same format (yyy-mm-dd). The recipes column(B) will contain a list of all of your recipes, do not change anything in this column.  The quantity column(C) are where you input the information for the transaction.  Simply enter a whole number for each recipe that was sold in the transaction.  Leave the 0 for any recipes that were not sold in the transaction.</p>
+
+            <p>NOTE: Each spreadsheet may only contain a single transaction.  If you need to enter more than on transaction, then you must change the data and upload the file again.</p>
+
             <h2 id="contact">CONTACT US</h2>
             <p>If you have found any problems with the site, or need help that is not covered here, please reach out to us.</p>
 

+ 21 - 8
views/shared/shared.css

@@ -159,6 +159,7 @@ form{
     font-size: 20px;
     font-weight: bold;
     min-width: 100px;
+    margin: 5px;
 } 
 
     .buttonWithBorder {
@@ -177,17 +178,29 @@ form{
 }
 
 .button-small{
-    background: none;
-    border: 2px solid rgb(255, 99, 107);
+    background: rgb(255, 99, 107);
+    border: none;
     text-decoration: none;
-    border-radius: 10px;
-    padding: 3px 5px;
-    color: #001b2d;
+    border-radius: 4px;
+    margin: 5px;
+    padding: 3px;
+    color: white;
+    transition: 0.3s;
+    text-align: center;
+    font-size: 15px;
+    font-weight: bold;
+    min-width: 100px;
+    cursor: pointer;
+}
+
+.linkButton{
+    background: none;
+    border: none;
+    text-decoration: underline;
+    color: blue;
     cursor: pointer;
+    margin: 5px;
     font-size: 15px;
-    box-shadow: 1px 1px 1px black;
-    margin: 0 2px;
-    transition: 0.3s; 
 }
 
 .public-buttons {

+ 1 - 1
views/verifyPage/verify.css

@@ -19,7 +19,7 @@ body{
     padding: 0;
 }
 
-    .code{
+    .email{
         font-size: 30px;
         margin-bottom: 15px;
         text-align: center;

+ 5 - 9
views/verifyPage/verify.ejs

@@ -12,21 +12,17 @@
     <body>
         <% include ../shared/header %>
 
-        <h1 class="title">Verify your email address</h1>
+        <h1 class="title">An email has been sent to <%=email%>.  Use the link in the email to verify your account.</h1>
 
-        <p class="text">Please enter the 15 character code sent to your email address</p>
+        <p class="text">If you did not recieve an email or need to change your email address, enter it below and click submit.</p>
 
-        <form class="form" action="/verify" method="post">
-            <input class="code" name="code" type="text">
+        <form class="form" action="/verify/resend" method="post">
+            <input class="email" name="email" type="email" required>
 
             <input name="id" type="hidden" value="<%=id%>">
 
-            <input class="button" type="submit" value="VERIFY">
+            <input class="button" type="submit" value="SUBMIT">
         </form>
-
-        <p class="resend">Didn't recieve an email?</p>
-
-        <a href="/verify/email/<%=id%>">Resend email</a>
     </body>
 
     <script>

Some files were not shown because too many files changed in this diff