analytics.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488
  1. const Transaction = require("../classes/Transaction.js");
  2. let analytics = {
  3. isPopulated: false,
  4. ingredient: undefined,
  5. category: undefined,
  6. recipe: undefined,
  7. recipeCategory: undefined,
  8. transactionsByDate: [],
  9. display: function(){
  10. if(!this.isPopulated){
  11. let analIngredientList = document.getElementById("analIngredientList");
  12. let analCategoriesList = document.getElementById("analCategoriesList");
  13. let analRecipeList = document.getElementById("analRecipeList");
  14. let analCatRecipeList = document.getElementById("analCatRecipeList");
  15. analIngredientList.onkeyup = ()=>{this.searchItems(analIngredientList.children)};
  16. analCategoriesList.onkeyup = ()=>{this.searchItems(analCategoriesList.children)};
  17. analRecipeList.onkeyup = ()=>{this.searchItems(analRecipeList.children)};
  18. analCatRecipeList.onkeyup = ()=>{this.searchItems(analCatRecipeList.children)};
  19. let ingredientTab = document.getElementById("analIngredientsTab");
  20. let recipeTab = document.getElementById("analRecipesTab");
  21. let categoryTab = document.getElementById("analCategoriesTab");
  22. let individualTab = document.getElementById("analIndividualTab");
  23. ingredientTab.onclick = ()=>{this.tab(ingredientTab)};
  24. categoryTab.onclick = ()=>{this.tab(categoryTab)};
  25. recipeTab.onclick = ()=>{this.tab(recipeTab)};
  26. individualTab.onclick = ()=>{this.tab(individualTab)};
  27. let to = new Date();
  28. let from = new Date(to.getFullYear(), to.getMonth() - 1, to.getDate());
  29. document.getElementById("analStartDate").valueAsDate = from;
  30. document.getElementById("analEndDate").valueAsDate = to;
  31. document.getElementById("analDateBtn").onclick = ()=>{this.newDates()};
  32. this.populateButtons();
  33. if(merchant.inventory.length > 0) this.ingredient = merchant.inventory[0].ingredient;
  34. if(merchant.recipes.length > 0) this.recipe = merchant.recipes[0];
  35. this.newDates();
  36. this.isPopulated = true;
  37. }
  38. },
  39. populateButtons: function(){
  40. let ingredientButtons = document.getElementById("analIngredientList");
  41. let categoryButtons = document.getElementById("analCategoriesList");
  42. let recipeButtons = document.getElementById("analRecipeList");
  43. let recipeCategoryButtons = document.getElementById("analCatRecipeList");
  44. while(ingredientButtons.children.length > 1){
  45. ingredientButtons.removeChild(ingredientButtons.lastChild);
  46. }
  47. for(let i = 0; i < merchant.inventory.length; i++){
  48. let button = document.createElement("button");
  49. button.innerText = merchant.inventory[i].ingredient.name;
  50. button.classList.add("choosable");
  51. button.onclick = ()=>{
  52. this.ingredient = merchant.inventory[i].ingredient;
  53. this.displayIngredient();
  54. };
  55. ingredientButtons.appendChild(button);
  56. }
  57. while(categoryButtons.children.length > 1){
  58. categoryButtons.removeChild(categoryButtons.lastChild);
  59. }
  60. let categories = merchant.categorizeIngredients();
  61. for(let i = 0; i < categories.length; i++){
  62. let button = document.createElement("button");
  63. button.innerText = categories[i].name;
  64. button.classList.add("choosable");
  65. button.onclick = ()=>{
  66. this.category = categories[i];
  67. this.displayIngredientCategory();
  68. }
  69. categoryButtons.appendChild(button);
  70. }
  71. while(recipeButtons.children.length > 1){
  72. recipeButtons.removeChild(recipeButtons.lastChild);
  73. }
  74. for(let i = 0; i < merchant.recipes.length; i++){
  75. let button = document.createElement("button");
  76. button.innerText = merchant.recipes[i].name;
  77. button.classList.add("choosable");
  78. button.onclick = ()=>{
  79. this.recipe = merchant.recipes[i];
  80. this.displayRecipe();
  81. };
  82. recipeButtons.appendChild(button);
  83. }
  84. while(recipeCategoryButtons.children.length > 1){
  85. recipeCategoryButtons.removeChild(recipeCategoryButtons.lastChild);
  86. }
  87. let recipeCategories = merchant.categorizeRecipes();
  88. for(let i = 0; i < recipeCategories.length; i++){
  89. let button = document.createElement("button");
  90. button.innerText = (recipeCategories[i].name === "") ? "UNCATEGORIZED" : recipeCategories[i].name;
  91. button.classList.add("choosable");
  92. button.onclick = ()=>{
  93. this.recipeCategory = recipeCategories[i];
  94. this.displayRecipeCategory();
  95. }
  96. recipeCategoryButtons.appendChild(button);
  97. }
  98. },
  99. getData: function(from, to){
  100. let data = {
  101. from: from,
  102. to: to,
  103. recipes: []
  104. }
  105. let loader = document.getElementById("loaderContainer");
  106. loader.style.display = "flex";
  107. return fetch("/transaction", {
  108. method: "post",
  109. headers: {
  110. "Content-Type": "application/json"
  111. },
  112. body: JSON.stringify(data)
  113. })
  114. .then(response => response.json())
  115. .then((response)=>{
  116. if(typeof(response) === "string"){
  117. controller.createBanner(response, "error");
  118. }else{
  119. this.transactionsByDate = [];
  120. response.reverse();
  121. let startOfDay = new Date(from.getTime());
  122. startOfDay.setHours(0, 0, 0, 0);
  123. let endOfDay = new Date(from.getTime());
  124. endOfDay.setDate(endOfDay.getDate() + 1);
  125. endOfDay.setHours(0, 0, 0, 0);
  126. let transactionIndex = 0;
  127. while(startOfDay <= to){
  128. let currentTransactions = [];
  129. while(transactionIndex < response.length && new Date(response[transactionIndex].date) < endOfDay){
  130. currentTransactions.push(new Transaction(
  131. response[transactionIndex]._id,
  132. response[transactionIndex].date,
  133. response[transactionIndex].recipes,
  134. merchant
  135. ));
  136. transactionIndex++;
  137. }
  138. let thing = {
  139. date: new Date(startOfDay.getTime()),
  140. transactions: currentTransactions
  141. };
  142. this.transactionsByDate.push(thing);
  143. startOfDay.setDate(startOfDay.getDate() + 1);
  144. endOfDay.setDate(endOfDay.getDate() + 1);
  145. }
  146. }
  147. })
  148. .catch((err)=>{
  149. controller.createBanner("UNABLE TO UPDATE THE PAGE", "error");
  150. })
  151. .finally(()=>{
  152. loader.style.display = "none";
  153. });
  154. },
  155. displayIngredient: function(){
  156. if(this.ingredient === undefined || this.transactionsByDate.length === 0) return;
  157. //break down data into dates and quantities
  158. let dates = [];
  159. let quantities = [];
  160. for(let i = 0; i < this.transactionsByDate.length; i++){
  161. dates.push(this.transactionsByDate[i].date);
  162. let sum = 0;
  163. for(let j = 0; j < this.transactionsByDate[i].transactions.length; j++){
  164. let transaction = this.transactionsByDate[i].transactions[j];
  165. sum += transaction.getIngredientQuantity(this.ingredient, true);
  166. }
  167. quantities.push(sum);
  168. }
  169. //create and display the graph
  170. let trace = {
  171. x: dates,
  172. y: quantities,
  173. mode: "lines+markers",
  174. line: {
  175. color: "rgb(255, 99, 107)"
  176. }
  177. }
  178. let unit = (this.ingredient.unit === "bottle") ? this.ingredient.altUnit : this.ingredient.unit;
  179. let yaxis = `QUANTITY (${unit.toUpperCase()})`;
  180. let layout = {
  181. title: this.ingredient.name.toUpperCase(),
  182. xaxis: {title: "DATE"},
  183. yaxis: {title: yaxis},
  184. margin: {
  185. l: 40,
  186. r: 10,
  187. b: 20,
  188. t: 30
  189. },
  190. paper_bgcolor: "rgba(0, 0, 0, 0)"
  191. }
  192. Plotly.newPlot("itemUseGraph", [trace], layout);
  193. //Create min/max/avg
  194. //Current ingredient is stored on the "analMinUse" element
  195. let min = quantities[0];
  196. let max = quantities[0];
  197. let sum = 0;
  198. for(let i = 0; i < quantities.length; i++){
  199. if(quantities[i] < min) min = quantities[i];
  200. if(quantities[i] > max) max = quantities[i];
  201. sum += quantities[i];
  202. }
  203. document.getElementById("analMinUse").innerText = `${min.toFixed(2)} ${unit.toUpperCase()}`;
  204. document.getElementById("analAvgUse").innerText = `${(sum / quantities.length).toFixed(2)} ${unit.toUpperCase()}`;
  205. document.getElementById("analMaxUse").innerText = `${max.toFixed(2)} ${unit.toUpperCase()}`;
  206. //Create weekday averages
  207. let dayUse = [0, 0, 0, 0, 0, 0, 0];
  208. let dayCount = [0, 0, 0, 0, 0, 0, 0];
  209. for(let i = 0; i < quantities.length; i++){
  210. dayUse[dates[i].getDay()] += quantities[i];
  211. dayCount[dates[i].getDay()]++;
  212. }
  213. document.getElementById("analDayOne").innerText = `${(dayUse[0] / dayCount[0]).toFixed(2)} ${unit.toUpperCase()}`;
  214. document.getElementById("analDayTwo").innerText = `${(dayUse[1] / dayCount[1]).toFixed(2)} ${unit.toUpperCase()}`;
  215. document.getElementById("analDayThree").innerText = `${(dayUse[2] / dayCount[2]).toFixed(2)} ${unit.toUpperCase()}`;
  216. document.getElementById("analDayFour").innerText = `${(dayUse[3] / dayCount[3]).toFixed(2)} ${unit.toUpperCase()}`;
  217. document.getElementById("analDayFive").innerText = `${(dayUse[4] / dayCount[4]).toFixed(2)} ${unit.toUpperCase()}`;
  218. document.getElementById("analDaySix").innerText = `${(dayUse[5] / dayCount[5]).toFixed(2)} ${unit.toUpperCase()}`;
  219. document.getElementById("analDaySeven").innerText = `${(dayUse[6] / dayCount[6]).toFixed(2)} ${unit.toUpperCase()}`;
  220. },
  221. displayIngredientCategory: function(){
  222. if(this.category === undefined) this.category = merchant.categorizeIngredients()[0];
  223. if(this.category === undefined) return;
  224. let dates = [];
  225. let quantities = [];
  226. for(let i = 0; i < this.transactionsByDate.length; i++){
  227. dates.push(this.transactionsByDate[i].date);
  228. let total = 0;
  229. for(let j = 0; j < this.transactionsByDate[i].transactions.length; j++){
  230. let transaction = this.transactionsByDate[i].transactions[j];
  231. for(let k = 0; k < this.category.ingredients.length; k++){
  232. let ingredient = this.category.ingredients[k].ingredient;
  233. total += transaction.getIngredientQuantity(ingredient, true) * ingredient.getUnitCost();
  234. }
  235. }
  236. quantities.push(total);
  237. }
  238. let trace = {
  239. x: dates,
  240. y: quantities,
  241. mode: "lines+markers",
  242. line: {
  243. color: "rgb(255, 99, 107)"
  244. }
  245. };
  246. let layout = {
  247. title: this.category.name,
  248. xaxis: {title: "DATE"},
  249. yaxis: {title: "COST ($)"},
  250. margin: {
  251. l: 40,
  252. r: 10,
  253. b: 20,
  254. t: 30
  255. },
  256. paper_bgcolor: "white"
  257. }
  258. Plotly.newPlot("analCategoriesGraph", [trace], layout);
  259. },
  260. displayRecipe: function(){
  261. if(this.recipe === undefined || this.transactionsByDate.length === 0) return;
  262. //break down data into dates and quantities
  263. let dates = [];
  264. let quantities = [];
  265. for(let i = 0; i < this.transactionsByDate.length; i++){
  266. dates.push(this.transactionsByDate[i].date);
  267. let sum = 0;
  268. for(let j = 0; j < this.transactionsByDate[i].transactions.length; j++){
  269. const transaction = this.transactionsByDate[i].transactions[j];
  270. for(let k = 0; k < transaction.recipes.length; k++){
  271. if(transaction.recipes[k].recipe === this.recipe){
  272. sum += transaction.recipes[k].quantity;
  273. }
  274. }
  275. }
  276. quantities.push(sum);
  277. }
  278. //create and display the graph
  279. const trace = {
  280. x: dates,
  281. y: quantities,
  282. mode: "lines+markers",
  283. line: {
  284. color: "rgb(255, 99, 107)"
  285. }
  286. }
  287. const layout = {
  288. title: this.recipe.name.toUpperCase(),
  289. xaxis: {title: "DATE"},
  290. yaxis: {title: "QUANTITY"},
  291. margin: {
  292. l: 40,
  293. r: 10,
  294. b: 20,
  295. t: 30
  296. },
  297. paper_bgcolor: "rgba(0, 0, 0, 0)"
  298. }
  299. Plotly.newPlot("recipeSalesGraph", [trace], layout);
  300. //Display the boxes at the bottom
  301. //Current recipe is stored on the "recipeAvgUse" element
  302. let avg = 0;
  303. for(let i = 0; i < quantities.length; i++){
  304. avg += quantities[i];
  305. }
  306. avg = avg / quantities.length;
  307. document.getElementById("recipeAvgUse").innerText = avg.toFixed(2);
  308. document.getElementById("recipeAvgRevenue").innerText = `$${(avg * this.recipe.price).toFixed(2)}`;
  309. },
  310. displayRecipeCategory: function(){
  311. if(this.recipeCategory === undefined) this.recipeCategory = merchant.categorizeRecipes()[0];
  312. let dates = [];
  313. let quantities = [];
  314. for(let i = 0; i < this.transactionsByDate.length; i++){
  315. dates.push(this.transactionsByDate[i].date);
  316. let total = 0;
  317. for(let j = 0; j < this.transactionsByDate[i].transactions.length; j++){
  318. for(let k = 0; k < this.recipeCategory.recipes.length; k++){
  319. total += this.transactionsByDate[i].transactions[j].getRecipeQuantity(this.recipeCategory.recipes[k]);
  320. total *= this.recipeCategory.recipes[k].price;
  321. }
  322. }
  323. quantities.push(total);
  324. }
  325. let trace = {
  326. x: dates,
  327. y: quantities,
  328. mode: "lines+markers",
  329. line: {
  330. color: "rgb(255, 99, 107)"
  331. }
  332. };
  333. let layout = {
  334. title: (this.recipeCategory.name === "") ? "UNCATEGORIZED" : this.recipeCategory.name,
  335. xaxis: {title: "DATE"},
  336. yaxis: {title: "REVENUE ($)"},
  337. margin: {
  338. l: 40,
  339. r: 10,
  340. b: 20,
  341. t: 30
  342. },
  343. paper_bgcolor: "white"
  344. };
  345. Plotly.newPlot("analCatRecipeGraph", [trace], layout);
  346. },
  347. newDates: async function(){
  348. const from = document.getElementById("analStartDate").valueAsDate;
  349. const to = document.getElementById("analEndDate").valueAsDate;
  350. from.setHours(0, 0, 0, 0);
  351. to.setDate(to.getDate() + 1);
  352. to.setHours(0, 0, 0, 0);
  353. await this.getData(from, to);
  354. let analTabs = document.getElementById("analTabs");
  355. if(analTabs.children[0].children[0].classList.contains("active")){
  356. if(analTabs.children[1].children[0].classList.contains("active")){
  357. this.displayIngredient();
  358. }else{this.displayIngredientCategory()};
  359. }else{
  360. if(analTabs.children[1].children[0].classList.contains("active")){
  361. this.displayRecipe();
  362. }else{this.displayRecipeCategory()};
  363. }
  364. },
  365. tab: function(tab){
  366. let upperGroup = document.getElementById("topAnalTabs");
  367. let lowerGroup = document.getElementById("bottomAnalTabs");
  368. let strand = document.getElementById("analyticsStrand");
  369. for(let i = 0; i < tab.parentElement.children.length; i++){
  370. tab.parentElement.children[i].classList.remove("active");
  371. }
  372. tab.classList.add("active");
  373. for(let i = 1; i < strand.children.length; i++){
  374. strand.children[i].style.display = "none";
  375. }
  376. if(upperGroup.children[0].classList.contains("active")){
  377. if(lowerGroup.children[0].classList.contains("active")){
  378. document.getElementById("analIndividualIngredient").style.display = "flex";
  379. this.displayIngredient();
  380. }else{
  381. document.getElementById("analCategoryIngredient").style.display = "flex";
  382. this.displayIngredientCategory();
  383. }
  384. }else{
  385. if(lowerGroup.children[0].classList.contains("active")){
  386. document.getElementById("analIndividualRecipe").style.display = "flex";
  387. this.displayRecipe();
  388. }else{
  389. document.getElementById("analCategoryRecipe").style.display = "flex";
  390. this.displayRecipeCategory();
  391. }
  392. }
  393. },
  394. searchItems: function(list){
  395. let searchString = list[0].value;
  396. if(searchString === ""){
  397. for(let i = 1; i < list.length; i++){
  398. list[i].style.display = "block";
  399. }
  400. }else{
  401. for(let i = 1; i < list.length; i++){
  402. if(!list[i].innerText.toLowerCase().includes(searchString)){
  403. list[i].style.display = "none";
  404. }else{
  405. list[i].style.display = "flex";
  406. }
  407. }
  408. }
  409. }
  410. }
  411. module.exports = analytics;