Просмотр исходного кода

Merge branch 'chartsGraphs' into development

Lee Morgan 6 лет назад
Родитель
Сommit
0e642e5c60

+ 2 - 2
controllers/merchantData.js

@@ -16,8 +16,8 @@ module.exports = {
     //Redirects to /dashboard
     createMerchantNone: function(req, res){
         if(req.body.password === req.body.confirmPassword){
-            var salt = bcrypt.genSaltSync(10);
-            var hash = bcrypt.hashSync(req.body.password, salt);
+            let salt = bcrypt.genSaltSync(10);
+            let hash = bcrypt.hashSync(req.body.password, salt);
 
             let merchant = new Merchant({
                 name: req.body.name,

+ 56 - 1
controllers/otherData.js

@@ -3,6 +3,7 @@ const axios = require("axios");
 
 const Merchant = require("../models/merchant");
 const Purchase = require("../models/purchase");
+const Transaction = require("../models/transaction");
 
 module.exports = {
     //POST - Creates a new purchase for a merchant
@@ -129,6 +130,60 @@ module.exports = {
                 req.session.error = "Error: Unable to retrieve data from Clover";
                 return res.redirect("/");
             });
-        
+    },
+
+    //POST - Gets transactions and purchases between 2 dates for a merchant
+    //Inputs:
+    //  req.body.from = start date
+    //  req.body.to = end date
+    //Returns:
+    //  transactions = list of transactions between the dates provided
+    //  purchases = list of purchases between the dates provided
+    getData: function(req, res){
+        if(!req.session.user){
+            req.session.error = "Must be logged in to do that";
+            return res.redirect("/");
+        }
+
+        let promiseList = [];
+
+        for(let i = 0; i < req.body.dates.length; i+=2){
+            promiseList.push(new Promise((resolve, reject)=>{
+                Transaction.find({merchant: req.session.user, date: {$gte: req.body.dates[i], $lt: req.body.dates[i+1]}},
+                    {date: 1, recipes: 1, _id: 0},
+                    {sort: {date: 1}})
+                    .then((transactions)=>{
+                        resolve(transactions);
+                    })
+                    .catch((err)=>{});
+            }));
+
+            promiseList.push(new Promise((resolve, reject)=>{
+                Purchase.find({merchant: req.session.user, date: {$gte: req.body.dates[i], $lt: req.body.dates[i+1]}},
+                    {date: 1, ingredients: 1, _id: 0},
+                    {sort: {date: 1}})
+                    .then((purchases)=>{
+                        resolve(purchases);
+                    })
+                    .catch((err)=>{})
+            }));
+        }
+
+        Promise.all(promiseList)
+            .then((response)=>{
+                let newList = [];
+
+                for(let i = 0; i < response.length; i+=2){
+                    newList.push({
+                        transactions: response[i],
+                        purchases: response[i+1]
+                    });
+                }
+
+                return res.json(newList);
+            })
+            .catch((err)=>{
+                return res.json("Error: unable to retrieve user data");
+            });
     }
 }

+ 9 - 7
controllers/renderer.js

@@ -142,16 +142,16 @@ module.exports = {
             return res.redirect("/");
         }
 
+        let date = new Date();
+        let firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
+        let lastDay = new Date();
+
         let merchTransPromise = new Promise((resolve, reject)=>{
             Merchant.findOne({_id: req.session.user}, 
                 {name: 1, inventory: 1, recipes: 1, _id: 0})
                 .populate("recipes")
                 .populate("inventory.ingredient")
                 .then((merchant)=>{
-                    let date = new Date();
-                    let firstDay = new Date(date.getFullYear(), date.getMonth(), 1);
-                    let lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
-
                     Transaction.find({merchant: req.session.user, date: {$gte: firstDay, $lt: lastDay}},
                         {date: 1, recipes: 1, _id: 0},
                         {sort: {date: 1}})
@@ -164,8 +164,9 @@ module.exports = {
         });
 
         let purchasePromise = new Promise((resolve, reject)=>{
-            Purchase.find({merchant: req.session.user},
-                {date: 1, ingredients: 1, _id: 0})
+            Purchase.find({merchant: req.session.user, date: {$gte: firstDay, $lt: lastDay}},
+                {date: 1, ingredients: 1, _id: 0},
+                {sort: {date: 1}})
                 .then((purchases)=>{
                     resolve(purchases);
                 })
@@ -177,7 +178,8 @@ module.exports = {
                 let data = {
                     merchant: response[0].merchant,
                     transactions: response[0].transactions,
-                    purchases: response[1]
+                    purchases: response[1],
+                    dates: [firstDay, lastDay]
                 }
 
                 return res.render("dataPage/data", {data: data});

+ 1 - 0
controllers/transactionData.js

@@ -116,6 +116,7 @@ module.exports = {
         function randomDate() {
             let now = new Date();
             let start = new Date();
+            // start.setDate(now.getDate() - 10);
             start.setFullYear(now.getFullYear() - 1);
             return new Date(start.getTime() + Math.random() * (now.getTime() - start.getTime()));
         }

+ 3 - 1
routes.js

@@ -40,9 +40,11 @@ module.exports = function(app){
     app.get("/cloverlogin", otherData.cloverRedirect);
     app.get("/cloverauth*", otherData.cloverAuth);
 
+    app.post("/getdata", otherData.getData);
+
     //Transactions
     app.post("/transactions", transactionData.getTransactions);
     app.get("/purchases", transactionData.getPurchases);
     app.post("/transactions/create", transactionData.createTransaction);  //Creates transaction for non-pos merchant
-    app.get("/populate", transactionData.populate);
+    app.get("/populatesometransactions", transactionData.populate);
 }

+ 75 - 5
views/dataPage/data.css

@@ -1,3 +1,9 @@
+body, html{
+    height: 100%;
+    margin: 0;
+    padding: 0;
+}
+
 #title{
     font-size: 45px;
     color: rgb(255, 99, 107);
@@ -5,6 +11,28 @@
     margin-top: 25px;
 }
 
+.checkboxDiv{
+    display: flex;
+}
+
+    .checkboxDiv > *{
+        margin: 2px;
+    }
+
+.dates{
+    display: flex;
+    justify-content: center;
+}
+
+    .dates > *{
+        margin: 10px;
+    }
+
+canvas{
+    flex-grow: 9;
+    height: 64vh;
+}
+
 /* Home Strand */
 #homeStrand{
     flex-direction: column;
@@ -29,21 +57,63 @@
 
 /* Ingredient Strand */
 #ingredientStrand{
-    justify-content: space-between;
+    flex-direction: column;
+    align-items: space-between;
 }
 
     #ingredientStrand > *{
         margin: 10px;
     }
 
-    canvas{
-        flex-grow: 10;
-        max-height: 60vh;
-        max-width: 90vw;
+    .canvasBox{
+        display: flex;
+        justify-content: space-between;
     }
 
     #ingredientOptions{
         flex-grow: 1;
         display: flex;
         flex-direction: column;
+    }
+
+/* Recipe Strand */
+#recipeStrand{
+    flex-direction: column;
+    align-items: space-between;
+}
+
+    #recipeStrand > *{
+        margin: 10px;
+    }
+
+    .canvasBox{
+        display: flex;
+        justify-content: space-between;
+    }
+
+    #recipeOptions{
+        flex-grow: 1;
+        display: flex;
+        flex-direction: column;
+    }
+
+/* Purchase Strand */
+#purchaseStrand{
+    flex-direction: column;
+    align-items: space-between;
+}
+
+    #purchaseStrand > *{
+        margin: 10px;
+    }
+
+    .canvasBox{
+        display: flex;
+        justify-content: space-between;
+    }
+
+    #recipeOptions{
+        flex-grow: 1;
+        display: flex;
+        flex-direction: column;
     }

+ 72 - 14
views/dataPage/data.ejs

@@ -19,16 +19,14 @@
         <div id="homeStrand" class="strand">
             <h2 id="month"></h2>
 
-            <div class="buttonBox">
-                <p>From: </p>
-
-                <input id="from" type="date">
-
-                <p>To: </p>
-
-                <input id="to" type="date">
-
-                <button id="dateSort" class="buttonDisabled">Submit</button>
+            <div class="dates">
+                <label>From:
+                    <input id="homeFrom" type="date" onchange="window.homeObj.newDates()">
+                </label>
+                
+                <label>To:
+                    <input id="homeTo" type="date" onchange="window.homeObj.newDates()">
+                </label>
             </div>
 
             <div class="buttonBox">
@@ -90,16 +88,76 @@
         </div>
 
         <div id="ingredientStrand" class="strand">
-            <div id="ingredientOptions"></div>
+            <div class="dates">
+                <label>From:
+                    <input id="ingredientFrom" type="date" onchange="window.ingredientObj.newDates()">
+                </label>
+                
+                <label>To:
+                    <input id="ingredientTo" type="date" onchange="window.ingredientObj.newDates()">
+                </label>
+            </div>
+
+            <div class="canvasBox">
+                <div id="ingredientOptions"></div>
+
+                <canvas></canvas>
+            </div>
+        </div>
+
+        <div id="recipeStrand" class="strand">
+            <div class="dates">
+                <label>From:
+                    <input id="recipeFrom" type="date" onchange="window.recipeObj.newDates()">
+                </label>
+                
+                <label>To:
+                    <input id="recipeTo" type="date" onchange="window.recipeObj.newDates()">
+                </label>
+            </div>
+
+            <div class="canvasBox">
+                <div id="recipeOptions"></div>
 
-            <canvas></canvas>
+                <canvas></canvas>
+            </div>
         </div>
 
-        <script>let data = <%- JSON.stringify(data); %></script>
+        <div id="purchaseStrand" class="strand">
+            <div class="dates">
+                <label>From:
+                    <input id="purchaseFrom" type="date" onchange="window.purchaseObj.newDates()">
+                </label>
+                
+                <label>To:
+                    <input id="purchaseTo" type="date" onchange="window.purchaseObj.newDates()">
+                </label>
+            </div>
+
+            <div class="canvasBox">
+                <div id="purchaseOptions"></div>
+
+                <canvas></canvas>
+            </div>
+        </div>
+
+        <script>
+            let data = <%- JSON.stringify(data); %>;
+
+            for(let transaction of data.transactions){
+                transaction.date = new Date(transaction.date);
+            }
+
+            for(let purchase of data.purchases){
+                purchase.date = new Date(purchase.date);
+            }
+        </script>
         <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
         <script src="/shared/validation.js"></script>
         <script src="/dataPage/home.js"></script>
-        <script src="dataPage/ingredient.js"></script>
+        <script src="/dataPage/ingredient.js"></script>
+        <script src="/dataPage/recipe.js"></script>
+        <script src="/dataPage/purchase.js"></script>
         <script src="/shared/controller.js"></script>
         <script src="/shared/graphs.js"></script>
         <script src="/dataPage/fetchData.js"></script>

+ 113 - 24
views/dataPage/fetchData.js

@@ -1,31 +1,120 @@
-window.dataLoaded = false;
+window.fetchData = function(from, to, callback){
+    retrieveDates = [];
 
-let date = new Date();
-let yearAgo = new Date(new Date().setFullYear(date.getFullYear() - 1));
+    //Compares dates to dates already stored and makes a list of those needed
+    let fromCopy = new Date(from);
+    for(let i = 0; i < data.dates.length; i+=2){
+        if(to <= new Date(data.dates[0]) || fromCopy >= new Date(data.dates[data.dates.length - 1])){
+            retrieveDates.push(fromCopy);
+            retrieveDates.push(to);
+            break;
+        }
 
-let onDataLoad = function(){
-    let dateSort = document.querySelector("#dateSort");
-    dateSort.onclick = ()=>{window.homeObj.newDates()};
-    dateSort.classList = "button";
+        let date0 = new Date(data.dates[i]);
+        let date1 = new Date(data.dates[i+1]);
 
-    let ingredientsBody = document.querySelector("#ingredientsData tbody");
-    for(let row of ingredientsBody.children){
-        row.classList = "clickableRow";
-        row.onclick = ()=>{window.ingredientObj.display("ingredient", row.ingredient)};
+        if(fromCopy < date0){
+            retrieveDates.push(fromCopy);
+            retrieveDates.push(date0);
+            fromCopy = date1;
+        }else if(fromCopy > date0 && fromCopy < date1){
+            fromCopy = date1;
+        }
     }
 
-    window.dataLoaded = true;
+    if(retrieveDates.length > 0){
+        axios.post("/getData", {dates: retrieveDates})
+            .then((response)=>{
+                if(typeof(response.data) === "string"){
+                    banner.createError(response.data);
+                }else{
+                    let mergedDates = [];
+                    let oldIndex = 0;
+                    let newIndex = 0;
+
+                    while(oldIndex + newIndex < retrieveDates.length + data.dates.length){
+                        let oldDateOne = new Date(data.dates[oldIndex]);
+                        let oldDateTwo = new Date(data.dates[oldIndex+1]);
+                        if(oldDateOne < retrieveDates[newIndex] || newIndex >= retrieveDates.length){
+                            if(mergedDates.length > 0 && oldDateOne.getTime() === mergedDates[mergedDates.length-1].getTime()){
+                                mergedDates[mergedDates.length-1] = oldDateTwo;
+                            }else{
+                                mergedDates.push(oldDateOne);
+                                mergedDates.push(oldDateTwo);
+                            }
+                            oldIndex += 2;
+                        }else{
+                            if(mergedDates.length > 0 && retrieveDates[newIndex].getTime() === mergedDates[mergedDates.length-1].getTime()){
+                                mergedDates[mergedDates.length-1] = retrieveDates[newIndex+1];
+                            }else{
+                                mergedDates.push(retrieveDates[newIndex]);
+                                mergedDates.push(retrieveDates[newIndex+1]);
+                            }
+                            newIndex += 2;
+                        }
+                    }
+
+                    data.dates = mergedDates;
+
+                    for(let set of response.data){
+                        for(let transaction of set.transactions){
+                            transaction.date = new Date(transaction.date);
+                            transaction.date.setMinutes(transaction.date.getMinutes() + transaction.date.getTimezoneOffset());
+                        }
+
+                        if(set.transactions[0].date < data.transactions[0].date){
+                            data.transactions = set.transactions.concat(data.transactions);
+                        }else if(set.transactions[set.transactions.length-1].date > data.transactions[data.transactions.length-1].date){
+                            data.transactions = data.transactions.concat(set.transactions);
+                        }else{
+                            for(let i = 0; i < data.transactions.length; i++){
+                                if(set.transactions[0].date > data.transactions[i].date){
+                                    data.transactions = data.transactions.slice(0, i).concat(set.transactions).concat(data.transactions.slice(i, data.transactions.length - 1));
+                                    break;
+                                }
+                            }
+                        }
+
+                        for(let purchase of set.purchases){
+                            purchase.date = new Date();
+                            purchase.date.setMinutes(purchase.date.getMinutes() + purchase.date.getTimezoneOffset());
+                        }
+
+                        if(set.purchases.length > 0){
+                            if(data.purchases.length === 0){
+                                data.purchases = set.purchases;
+                            }else if(set.purchases[0].date < data.purchases[0].date){
+                                data.purchases = set.purchases.concat(data.purchases);
+                            }else if(set.purchases[set.purchases.length-1].date > data.purchases[data.purchases.length-1].date){
+                                data.purchases = data.purchases.concat(set.purchases);
+                            }else{
+                                for(let i = 0; i < data.purchases.length; i++){
+                                    if(set.purchases[0].date > data.purchases[i].date){
+                                        data.purchases = data.purchases.slice(0, i).concat(set.purchases).concat(data.purchases.slice(i, data.purchases.length - 1));
+                                        break;
+                                    }
+                                }
+                            }
+                        }
+                    }
+
+                    callback();
+                }
+            })
+            .catch((err)=>{});
+    }else{
+
+        callback();
+    }
 }
 
-axios.post("/transactions", {from: yearAgo, to: date})
-    .then((response)=>{
-        if(typeof(response.data) === "string"){
-            banner.createError(response.data);
-        }else{
-            data.transactions = response.data;
-            onDataLoad();
-        }
-    })
-    .catch((err)=>{
-        console.log(err);
-    });
+window.getInputDates = function(name){
+    let from = document.querySelector(`#${name}From`).valueAsDate;
+    let to = document.querySelector(`#${name}To`).valueAsDate;
+
+    from.setMinutes(from.getMinutes() + from.getTimezoneOffset());
+    to.setMinutes(to.getMinutes() + to.getTimezoneOffset());
+    to.setDate(to.getDate() + 1);
+
+    return [from, to];
+}

+ 37 - 30
views/dataPage/home.js

@@ -8,15 +8,14 @@ window.homeObj = {
     display: function(){
         clearScreen();
         document.querySelector("#homeStrand").style.display = "flex";
-
-        //Fill in month
-        let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
-        document.querySelector("#month").innerText = `Month of ${months[new Date().getMonth()]}`;
-
-        document.querySelector("#to").valueAsDate = new Date();
-
+        
         if(!this.isPopulated){
             this.populate(data.transactions);
+
+            let date = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
+            document.querySelector("#homeFrom").valueAsDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12);
+            document.querySelector("#homeTo").valueAsDate = new Date();
+
             this.isPopulated = true;
         }
     },
@@ -61,6 +60,7 @@ window.homeObj = {
         }
         
         //Populate number of recipes sold
+        let i = 0;
         for(let transaction of transactions){
             for(let recipe of transaction.recipes){
                 for(let newRecipe of recipes){
@@ -108,12 +108,9 @@ window.homeObj = {
         for(let ingredient of soldIngredients){
             let row = document.createElement("tr");
             ingredientsBody.appendChild(row);
-            if(window.dataLoaded){
-                row.classList = "clickableRow";
-                row.onclick = ()=>{window.ingredientObj.display("ingredient", ingredient)};
-            }else{
-                row.ingredient = ingredient;
-            }
+            row.classList = "clickableRow";
+            row.onclick = ()=>{window.ingredientObj.display(ingredient)};
+            
 
             let name = document.createElement("td");
             name.innerText = `${ingredient.name} (${ingredient.unit})`;
@@ -137,6 +134,8 @@ window.homeObj = {
 
         for(let recipe of recipes){
             let row = document.createElement("tr");
+            row.classList = "clickableRow";
+            row.onclick = ()=>{window.recipeObj.display(recipe)};
             recipesBody.appendChild(row);
 
             let name = document.createElement("td");
@@ -161,6 +160,8 @@ window.homeObj = {
         
         for(let ingredient of purchaseIngredients){
             let row = document.createElement("tr");
+            row.classList = "clickableRow";
+            row.onclick = ()=>{window.purchaseObj.display(ingredient)};
             purchasesBody.appendChild(row);
             
             let name = document.createElement("td");
@@ -178,8 +179,8 @@ window.homeObj = {
     },
 
     newDates: function(){
-        let from = new Date(document.querySelector("#from").value);
-        let to = new Date(document.querySelector("#to").value);
+        let from = document.querySelector("#homeFrom").value;
+        let to = document.querySelector("#homeTo").value;
 
         if(from === "" || to === ""){
             banner.createError("Invalid date");
@@ -187,27 +188,33 @@ window.homeObj = {
         }else{
             from = new Date(from);
             to = new Date(to);
+
+            from.setMinutes(from.getMinutes() + from.getTimezoneOffset());
+            to.setMinutes(to.getMinutes() + to.getTimezoneOffset());
         }
 
         if(validator.transaction.date(from, to)){
-            let startIndex = 0;
-            let endIndex = data.transactions.length;
+            window.fetchData(from, to, ()=>{
+                let startIndex = 0;
+                let endIndex = data.transactions.length;
 
-            for(let i = 0; i < data.transactions.length; i++){
-                if(from < new Date(data.transactions[i].date)){
-                    startIndex = i;
-                    break;
+                for(let i = 0; i < data.transactions.length; i++){
+                    if(from < data.transactions[i].date){
+                        startIndex = i;
+                        break;
+                    }
                 }
-            }
-
-            for(let i = 0; i < data.transactions.length; i++){
-                if(to < new Date(data.transactions[i].date)){
-                    endIndex = i;
-                    break;
+    
+                for(let i = 0; i < data.transactions.length; i++){
+                    if(to < data.transactions[i].date){
+                        endIndex = i;
+                        break;
+                    }
                 }
-            }
-
-            this.populate(data.transactions.slice(startIndex, endIndex));
+    
+                let newData = data.transactions.slice(startIndex, endIndex);
+                this.populate(newData);
+            });
         }
     }
 }

+ 73 - 38
views/dataPage/ingredient.js

@@ -2,81 +2,116 @@ window.ingredientObj = {
     isPopulated: false,
     graph: {},
 
-    display: function(type, ingredient){
+    display: function(ingredient){
         clearScreen();
         document.querySelector("#ingredientStrand").style.display = "flex";
         document.querySelector("strand-selector").setAttribute("strand", "ingredient");
 
         if(!this.isPopulated){
+            let date = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
+            document.querySelector("#ingredientFrom").valueAsDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12);
+            document.querySelector("#ingredientTo").valueAsDate = new Date();
+
             let ingredientsDiv = document.querySelector("#ingredientOptions");
 
             for(let item of data.merchant.inventory){
-                let label = document.createElement("label");
-                label.innerText = item.ingredient.name;
-                ingredientsDiv.appendChild(label);
-                
+                let checkDiv = document.createElement("div");
+                checkDiv.classList = "checkboxDiv";
+                ingredientsDiv.appendChild(checkDiv);
+
                 let checkbox = document.createElement("input");
                 checkbox.type = "checkbox";
+                checkbox.id = `${item.ingredient.name}Checkbox`;
+                checkbox._id = item.ingredient._id;
+                checkbox.name = item.ingredient.name;
                 checkbox.onchange = ()=>{
                     if(checkbox.checked){
-                        this.graph.addData(this.formatData("ingredient", item.ingredient._id));
+                        let from, to;
+                        [from, to] = getInputDates("ingredient");
+
+                        this.graph.addData(this.formatData(item.ingredient._id, from, to), [from, to], item.ingredient.name);
                     }else{
-                        this.graph.removeData(item.ingredient._id);
+                        this.graph.removeData(item.ingredient.name);
                     }
                 };
-                label.appendChild(checkbox);
-            }
+                checkDiv.appendChild(checkbox);
 
-            let startDate = new Date();
-            startDate.setFullYear(new Date().getFullYear() - 1);
+                let label = document.createElement("label");
+                label.innerText = item.ingredient.name;
+                label.setAttribute("for", `${item.ingredient.name}Checkbox`);
+                checkDiv.appendChild(label);
+            }
 
             this.graph = new LineGraph(
                 document.querySelector("#ingredientStrand canvas"),
                 "Quantity",
                 "Date",
-                {
-                    type: "date",
-                    start: startDate,
-                    end: new Date()
-                }
             );
 
             this.isPopulated = true;
         }
 
         if(ingredient){
-            this.graph.addData(this.formatData("ingredient", ingredient.id));
+            this.graph.clearData();
+
+            let from, to;
+            [from, to] = getInputDates("ingredient");
+            
+            this.graph.addData(this.formatData(ingredient.id, from, to), [from, to], ingredient.name);
+
+            for(let label of document.querySelector("#ingredientOptions").children){
+                if(label.innerText === ingredient.name){
+                    label.children[0].checked = true;
+                }else{
+                    label.children[0].checked = false;
+                }
+            }
         }
     },
 
-    formatData: function(type, id){
-        dataList = new Array(365).fill(0);
-        let today = new Date();
-        
-        if(type === "ingredient"){
-            let dataLastDate = new Date(data.transactions[0].date);
-            let dateRange = Math.floor((Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()) - Date.UTC(dataLastDate.getFullYear(), dataLastDate.getMonth(), dataLastDate.getDate())) / (1000 * 60 * 60 * 24));
-
-            for(let transaction of data.transactions){
-                let transDate = new Date(transaction.date);
-                let diff = Math.floor((Date.UTC(transDate.getFullYear(), transDate.getMonth(), transDate.getDate()) - Date.UTC(today.getFullYear(), today.getMonth(), today.getDate())) / (1000 * 60 * 60 * 24));
-                if(diff <= 0){
-                    for(let recipe of transaction.recipes){
-                        for(let merchRecipe of data.merchant.recipes){
-                            if(merchRecipe._id === recipe.recipe){
-                                for(let ingredient of merchRecipe.ingredients){
-                                    if(ingredient.ingredient === id){
-                                        dataList[dateRange - Math.abs(diff)] += ingredient.quantity;
-                                    }
+    //TODO This can be made to be faster, no need to search full data list
+    formatData: function(id, startDate, endDate){
+        let dateRange = Math.floor((Date.UTC(endDate.getFullYear(), endDate.getMonth(), endDate.getDate()) - Date.UTC(startDate.getFullYear(), startDate.getMonth(), startDate.getDate())) / (1000 * 60 * 60 * 24)) + 1;
+        let dataList = new Array(Math.abs(dateRange)).fill(0);
+
+        for(let transaction of data.transactions){
+            let diff = Math.floor((Date.UTC(transaction.date.getFullYear(), transaction.date.getMonth(), transaction.date.getDate()) - Date.UTC(endDate.getFullYear(), endDate.getMonth(), endDate.getDate())) / (1000 * 60 * 60 * 24));
+
+            if(transaction.date > startDate && diff <= 0){
+                for(let recipe of transaction.recipes){
+                    for(let merchRecipe of data.merchant.recipes){
+                        if(merchRecipe._id === recipe.recipe){
+                            for(let ingredient of merchRecipe.ingredients){
+                                if(ingredient.ingredient === id){
+                                    dataList[dateRange - Math.abs(diff) - 1] += ingredient.quantity * recipe.quantity;
                                 }
-                                break;
                             }
+                            break;
                         }
                     }
                 }
             }
+        }
+
+        return dataList;
+    },
+
+    newDates: function(){
+        let from, to;
+        [from, to] = getInputDates("ingredient");
+
+        if(validator.transaction.date(from, to)){
+            fetchData(from, to, ()=>{
+                this.graph.clearData();
 
-            return {id: id, set: dataList};
+                let ingredientsDiv = document.querySelector("#ingredientOptions");
+                for(let div of ingredientsDiv.children){
+                    let checkbox = div.children[0];
+                    if(checkbox.checked){
+                        this.graph.addData(this.formatData(checkbox._id, from, to), [from, to], checkbox.name);
+                    }
+                }
+            });
         }
     }
 }

+ 109 - 0
views/dataPage/purchase.js

@@ -0,0 +1,109 @@
+window.purchaseObj = {
+    isPopulated: false,
+    graph: {},
+
+    display: function(ingredient){
+        clearScreen();
+        document.querySelector("#purchaseStrand").style.display = "flex";
+        document.querySelector("strand-selector").setAttribute("strand", "purchase");
+
+        if(!this.isPopulated){
+            let date = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
+            document.querySelector("#purchaseFrom").valueAsDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12);
+            document.querySelector("#purchaseTo").valueAsDate = new Date();
+
+            let purchasesDiv = document.querySelector("#purchaseOptions");
+
+            for(let item of data.merchant.inventory){
+                console.log(item);
+                let checkDiv = document.createElement("div");
+                checkDiv.classList = "checkboxDiv";
+                purchasesDiv.appendChild(checkDiv);
+
+                let checkbox = document.createElement("input");
+                checkbox.type = "checkbox";
+                checkbox.id = `${item.ingredient.name}PurchaseCheckbox`;
+                checkbox._id = item.ingredient._id;
+                checkbox.name = item.ingredient.name;
+                checkbox.onchange = ()=>{
+                    if(checkbox.checked){
+                        let from, to;
+                        [from, to] = getInputDates("purchase");
+
+                        this.graph.addData(this.formatData(item.ingredient._id, from, to), [from, to], item.ingredient.name);
+                    }else{
+                        this.graph.removeData(item.ingredient.name);
+                    }
+                };
+                checkDiv.appendChild(checkbox);
+
+                let label = document.createElement("label");
+                label.innerText = item.ingredient.name;
+                label.setAttribute("for", `${item.ingredient.name}PurchaseCheckbox`);
+                checkDiv.appendChild(label);
+            }
+
+            this.graph = new LineGraph(
+                document.querySelector("#purchaseStrand canvas"),
+                "Quantity",
+                "Date"
+            );
+
+            this.isPopulated = true;
+        }
+
+        if(ingredient){
+            this.graph.clearData();
+
+            let from, to;
+            [from, to] = getInputDates("purchase");
+
+            this.graph.addData(this.formatData(ingredient.id, from, to), [from, to], ingredient.name);
+
+            for(let label of document.querySelector("#purchaseOptions").children){
+                if(label.innerText === ingredient.name){
+                    label.children[0].checked = true;
+                }else{
+                    label.children[0].checked = false;
+                }
+            }
+        }
+    },
+
+    //TODO This can be made to be faster, no need to search full data list
+    formatData: function(id, from, to){
+        let dateRange = Math.floor((Date.UTC(to.getFullYear(), to.getMonth(), to.getDate()) - Date.UTC(from.getFullYear(), from.getMonth(), from.getDate())) / (1000 * 60 * 60 * 24)) + 1;
+        let dataList = new Array(Math.abs(dateRange)).fill(0);
+        
+        for(let purchase of data.purchases){
+            let diff = Math.floor((Date.UTC(purchase.date.getFullYear(), purchase.date.getMonth(), purchase.date.getDate()) - Date.UTC(to.getFullYear(), to.getMonth(), to.getDate())) / (1000 * 60 * 60 * 24));
+
+            for(let ingredient of purchase.ingredients){
+                if(purchase.date > from && diff <=0 && id === ingredient.ingredient){
+                    dataList[dateRange - Math.abs(diff) - 1] += ingredient.quantity;
+                }
+            }
+        }
+
+        return dataList;
+    },
+
+    newDates: function(){
+        let from, to;
+        [from, to] = getInputDates("purchase");
+
+        if(validator.transaction.date(from, to)){
+            fetchData(from, to, ()=>{
+                this.graph.clearData();
+
+                let purchaseDiv = document.querySelector("#purchaseOptions");
+                for(let div of purchaseDiv.children){
+                    let checkbox = div.children[0];
+                    if(checkbox.checked){
+                        this.graph.addData(this.formatData(checkbox._id, from, to), [from, to], checkbox.name);
+                    }
+                }
+            });
+        }
+    }
+}

+ 112 - 0
views/dataPage/recipe.js

@@ -0,0 +1,112 @@
+window.recipeObj = {
+    isPopulated: false,
+    graph: {},
+
+    display: function(recipe){
+        clearScreen();
+        document.querySelector("#recipeStrand").style.display = "flex";
+        document.querySelector("strand-selector").setAttribute("strand", "recipe");
+
+        if(!this.isPopulated){
+            let date = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
+            document.querySelector("#recipeFrom").valueAsDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 12);
+            document.querySelector("#recipeTo").valueAsDate = new Date();
+
+            let recipesDiv = document.querySelector("#recipeOptions");
+
+            for(let recipe of data.merchant.recipes){
+                let checkDiv = document.createElement("div");
+                checkDiv.classList = "checkboxDiv";
+                recipesDiv.appendChild(checkDiv);
+
+                let checkbox = document.createElement("input");
+                checkbox.type = "checkbox";
+                checkbox.id = `${recipe.name}Checkbox`;
+                checkbox._id = recipe._id;
+                checkbox.name = recipe.name;
+                checkbox.onchange = ()=>{
+                    if(checkbox.checked){
+                        let from, to;
+                        [from, to] = getInputDates("recipe");
+
+                        this.graph.addData(this.formatData(recipe._id, from, to), [from, to], recipe.name);
+                    }else{
+                        this.graph.removeData(recipe.name);
+                    }
+                };
+                checkDiv.appendChild(checkbox);
+
+                let label = document.createElement("label");
+                label.innerText = recipe.name;
+                label.setAttribute("for", `${recipe.name}Checkbox`);
+                checkDiv.appendChild(label);
+            }
+
+            this.graph = new LineGraph(
+                document.querySelector("#recipeStrand canvas"),
+                "Quantity",
+                "Date"
+            );
+
+            this.isPopulated = true;
+        }
+
+        if(recipe){
+            this.graph.clearData();
+
+            let from, to;
+            [from, to] = getInputDates("recipe");
+
+            this.graph.addData(this.formatData(recipe.id, from, to), [from, to], recipe.name);
+
+            for(let label of document.querySelector("#recipeOptions").children){
+                if(label.innerText === recipe.name){
+                    label.children[0].checked = true;
+                }else{
+                    label.children[0].checked = false;
+                }
+            }
+        }
+    },
+
+    //TODO This can be made to be faster, no need to search full data list
+    formatData: function(id, from, to){
+        let dateRange = Math.floor((Date.UTC(to.getFullYear(), to.getMonth(), to.getDate()) - Date.UTC(from.getFullYear(), from.getMonth(), from.getDate())) / (1000 * 60 * 60 * 24)) + 1;
+        let dataList = new Array(Math.abs(dateRange)).fill(0);
+
+        for(let transaction of data.transactions){
+            let diff = Math.floor((Date.UTC(transaction.date.getFullYear(), transaction.date.getMonth(), transaction.date.getDate()) - Date.UTC(to.getFullYear(), to.getMonth(), to.getDate())) / (1000 * 60 * 60 * 24));
+
+            if(transaction.date > from && diff <= 0){
+                for(let recipe of transaction.recipes){
+                    if(recipe.recipe === id){
+                        dataList[dateRange - Math.abs(diff) - 1] += recipe.quantity;
+                    }
+                }
+            }
+        }
+
+        return dataList;
+    },
+
+    
+
+    newDates: function(){
+        let from, to;
+        [from, to] = getInputDates("recipe");
+
+        if(validator.transaction.date(from, to)){
+            window.fetchData(from, to, ()=>{
+                this.graph.clearData();
+
+                let recipesDiv = document.querySelector("#recipeOptions");
+                for(let div of recipesDiv.children){
+                    let checkbox = div.children[0];
+                    if(checkbox.checked){
+                        this.graph.addData(this.formatData(checkbox._id, from, to), [from, to], checkbox.name);
+                    }
+                }
+            })
+        }
+    }
+}

+ 120 - 42
views/shared/graphs.js

@@ -1,57 +1,90 @@
+//Creates a line graph within a canvas
+//Will expand or shrink to the size of the canvas
+//Inputs:
+//  canvas = the canvas that you would like to draw on
+//  yName = a string for the name of the Y axis
+//  xName = a string for the name of the X axis
 class LineGraph{
-    constructor(canvas, yName, xName, xData){
+    constructor(canvas, yName, xName){
         canvas.height = canvas.clientHeight;
         canvas.width = canvas.clientWidth;
 
         this.canvas = canvas;
         this.context = canvas.getContext("2d");
         this.left = canvas.clientWidth - (canvas.clientWidth * 0.9);
-        this.right = canvas.clientWidth * 0.9;
-        this.top = canvas.clientHeight - (canvas.clientHeight * 0.9);
+        this.right = canvas.clientWidth * 0.8;
+        this.top = canvas.clientHeight - (canvas.clientHeight * 0.99);
         this.bottom = canvas.clientHeight * 0.9;
         this.data = [];
         this.max = 0;
         this.xName = xName;
         this.yName = yName;
-        this.xData = xData;
-    }
+        this.xRange = [];
+        this.colors = [];
+        this.colorIndex = 0;
 
-    drawGraph(){
-        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
-        this.drawYAxis();
-        this.drawXAxis();
+        for(let i = 0; i < 100; i++){
+            let redRand = Math.floor(Math.random() * 200);
+            let greenRand = Math.floor(Math.random() * 200);
+            let blueRand = Math.floor(Math.random() * 200);
 
-        for(let dataSet of this.data){
-            this.drawData(dataSet.set);
+            this.colors.push(`rgb(${redRand}, ${greenRand}, ${blueRand})`);
         }
     }
 
-    addData(data){
+    //Add a dataset to the graph to draw
+    //Inputs:
+    //  data = array containing list of numbers as the data points for the graph
+    //      data[0] will be on the left.  data[data.length-1] will be on the right.
+    //  xRange = array containing two elements, start and end for x axis data (currently only dates)
+    //  name = string name for the line.  Used for display and finding lines.  Each must be unique
+    addData(data, xRange, name){
+        console.log(data);
+        data = {
+            set: data,
+            colorIndex: this.colorIndex,
+            name: name
+        }
+        this.colorIndex++;
         this.data.push(data);
 
-        let isNewMax = false;
+        let isChange = false;
         for(let point of data.set){
             if(point > this.max){
                 this.max = point;
-                isNewMax = true;
+                this.verticalMultiplier = (this.bottom - this.top) / this.max;
+                this.horizontalMultiplier = (this.right - this.left) / (data.set.length - 1);
+                isChange = true;
             }
         }
 
-        if(isNewMax){
-            this.verticalMultiplier = (this.bottom - this.top) / this.max;
-            this.horizontalMultiplier = (this.right - this.left) / data.set.length;
-            
-            this.xData.dataLength = data.set.length;
+        if(this.xRange.length === 0){
+            this.xRange = xRange;
+            isChange = true;
+        }else{
+            if(xRange[0] < this.xRange[0]){
+                this.xRange[0] = xRange[0];
+                isChange = true;
+            }
+            if(xRange[1] > this.xRange[1]){
+                this.xRange[1] = xRange[1];
+                isChange = true;
+            }
+        }
 
+        if(isChange){
             this.drawGraph();
         }else{
-            this.drawData(data.set);
+            this.drawLine(data);
         }
     }
 
-    removeData(id){
+    //Removes a single data set from the graph and its line
+    //Inputs:
+    //  id = the unique identifier of the data set that was passed in with addData function
+    removeData(name){
         for(let i = 0; i < this.data.length; i++){
-            if(this.data[i].id === id){
+            if(this.data[i].name === name){
                 this.data.splice(i, 1);
                 break;
             }
@@ -60,26 +93,48 @@ class LineGraph{
         this.drawGraph();
     }
 
-    drawData(dataSet){
-        let redRand = Math.floor(Math.random() * 200);
-        let greenRand = Math.floor(Math.random() * 200);
-        let blueRand = Math.floor(Math.random() * 200);
+    //Completely clears all data
+    //Does not delete the current graph displaying
+    clearData(){
+        this.max = 0;
+        this.data = [];
+        this.xRange = [];
+    }
 
-        for(let i = 0; i < dataSet.length - 1; i++){
+    /**********
+    *********PRIVATE*********
+    **********/
+    drawGraph(){
+        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+        this.drawYAxis();
+        this.drawXAxis();
+
+        for(let dataSet of this.data){
+            this.drawLine(dataSet);
+        }
+    }
+
+    drawLine(data){
+        for(let i = 0; i < data.set.length - 1; i++){
             this.context.beginPath();
-            this.context.moveTo(this.left + (this.horizontalMultiplier * i), this.bottom - (this.verticalMultiplier * dataSet[i]));
-            this.context.lineTo(this.left + (this.horizontalMultiplier * (i + 1)), this.bottom - (this.verticalMultiplier * dataSet[i + 1]));
-            this.context.lineWidth = 1;
-            this.context.strokeStyle = `rgb(${redRand}, ${greenRand}, ${blueRand})`;
+            this.context.moveTo(this.left + (this.horizontalMultiplier * i), this.bottom - (this.verticalMultiplier * data.set[i]));
+            this.context.lineTo(this.left + (this.horizontalMultiplier * (i + 1)), this.bottom - (this.verticalMultiplier * data.set[i + 1]));
+            this.context.strokeStyle = this.colors[data.colorIndex];
+            this.context.lineWidth = 2;
             this.context.stroke();
         }
+
+        this.context.strokeStyle = "black";
+
+        this.drawLegend(data.colorIndex, data.name);
     }
 
     drawXAxis(){
         this.context.beginPath();
         this.context.moveTo(this.left, this.bottom);
         this.context.lineTo(this.right, this.bottom);
-        this.context.lineWidth = 2;
+        this.context.lineWidth = 4;
         this.context.stroke();
 
         this.context.font = "25px Arial";
@@ -89,21 +144,24 @@ class LineGraph{
         this.context.font = "10px Arial";
         this.context.lineWidth = 1;
 
-        if(this.xData.type = "date"){
-            let diff = Math.abs(Math.floor((Date.UTC(this.xData.start.getFullYear(), this.xData.start.getMonth(), this.xData.start.getDate()) - Date.UTC(this.xData.end.getFullYear(), this.xData.end.getMonth(), this.xData.end.getDate())) / (1000 * 60 * 60 * 24)));
-            let showDate = this.xData.start;
-
-            for(let i = 0; i < this.xData.dataLength; i += Math.floor(this.xData.dataLength / 10)){
+        if(Object.prototype.toString.call(this.xRange[0]) === '[object Date]'){
+            let diff = Math.abs(Math.floor((Date.UTC(this.xRange[0].getFullYear(), this.xRange[0].getMonth(), this.xRange[0].getDate()) - Date.UTC(this.xRange[1].getFullYear(), this.xRange[1].getMonth(), this.xRange[1].getDate())) / (1000 * 60 * 60 * 24))) + 1;
+            let showDate = new Date(this.xRange[0]);
+            
+            for(let i = 0; i < diff; i += Math.floor(diff / 10)){
                 this.context.fillText(showDate.toLocaleDateString("en-US", {month: "short", day: "numeric", year: "2-digit"}), this.left + (this.horizontalMultiplier * i) - 20, this.bottom + 15);
 
-                this.context.beginPath()
-                this.context.moveTo(this.left + (this.horizontalMultiplier * i), this.bottom);
-                this.context.lineTo(this.left + (this.horizontalMultiplier * i), this.top);
-                this.context.strokeStyle = "#a5a5a5";
-                this.context.stroke();
+                if(i !== 0){
+                    this.context.beginPath()
+                    this.context.moveTo(this.left + (this.horizontalMultiplier * i), this.bottom);
+                    this.context.lineTo(this.left + (this.horizontalMultiplier * i), this.top);
+                    this.context.strokeStyle = "#a5a5a5";
+                    this.context.stroke();
+                }
 
                 showDate.setDate(showDate.getDate() + Math.abs(diff / 10));
             }
+            
         }
 
         this.context.strokeStyle = "black";
@@ -143,4 +201,24 @@ class LineGraph{
         this.context.strokeStyle = "black";
         this.context.setLineDash([]);
     }
+
+    drawLegend(colorIndex, name){
+        let verticalOffset;
+        for(let i = 0; i < this.data.length; i++){
+            if(this.data[i].name === name){
+                verticalOffset = i * 25;
+                break;
+            }
+        }
+
+        this.context.beginPath();
+        this.context.fillStyle = this.colors[colorIndex];
+        this.context.fillRect(this.right + 50, this.top + 50 + verticalOffset, 10, 10);
+        this.context.stroke();
+
+        this.context.font = "15px Arial";
+        this.context.fillText(name, this.right + 65, this.top + 60 + verticalOffset);
+
+        this.context.fillStyle = "black";
+    }
 }

+ 1 - 1
views/shared/validation.js

@@ -119,7 +119,7 @@ let validator = {
                 errors.push("Starting date must be before ending date");
             }
 
-            if(from > today || to > today){
+            if(from > today || to > today.setDate(today.getDate() + 1)){
                 errors.push("Cannot choose a date in the future");
             }