VTPortal total time calculator

Make VTPortal calculate the total worked time. Written by @alopez

  1. // ==UserScript==
  2. // @name VTPortal total time calculator
  3. // @description Make VTPortal calculate the total worked time. Written by @alopez
  4. // @copyright 2023, Aritz
  5. // @license MIT
  6. // @version 5
  7. // @author Aritz Lopez
  8. // @collaborator Marcos Ruiz
  9. // @grant none
  10. // @match https://sign3910.visualtime.net/*
  11. // @match https://vtportal.visualtime.net/*
  12. // @namespace https://greasyfork.org/users/855840
  13. // ==/UserScript==
  14.  
  15. /* jshint esversion: 10 */
  16.  
  17. function update_total_time() {
  18. if (!document.querySelector("div#punchesList")) return;
  19. if (window.eval('i18nextko.i18n.lng();') !== last_language_code) {
  20. update_language_data().then(() => {
  21. update_total_time();
  22. });
  23. return;
  24. }
  25.  
  26. let totalElement = document.querySelector("span#totalTimeElement");
  27. if (!totalElement) {
  28. totalElement = document.createElement("span");
  29. totalElement.id = "totalTimeElement"
  30. totalElement.style.fontSize = "1.5rem";
  31.  
  32. if (document.querySelector('div[data-options*="punchesHome"]').nextElementSibling) {
  33. totalElement.style.marginLeft = '20%';
  34. totalElement.style.top = '1rem';
  35. totalElement.style.position = 'relative';
  36. const divider = document.querySelector('div[data-options*="punchesHome"]').nextElementSibling;
  37. divider.parentNode.insertBefore(totalElement, divider.nextSibling);
  38. } else if (document.querySelector("div#punchesList")) {
  39. document.querySelector("div#punchesList").appendChild(totalElement);
  40. }
  41. }
  42.  
  43. let remainingTimeElement = document.querySelector("span#remainingTime");
  44. if (!remainingTimeElement)
  45. {
  46. remainingTimeElement = document.createElement("span");
  47. remainingTimeElement.id = "remainingTime"
  48. remainingTimeElement.style.fontSize = "1.5rem";
  49. remainingTimeElement.style.top = totalElement.style.top;
  50. remainingTimeElement.style.position = 'relative';
  51. totalElement.parentNode.insertBefore(remainingTimeElement, totalElement.nextSibling);
  52. }
  53.  
  54. const punches = Array.prototype.map.call(
  55. document.querySelectorAll('div#punchesList div[data-bind="text: $data.Name"]'),
  56. function (d) { return d.innerHTML }
  57. )
  58.  
  59. let totalTime = 0;
  60. let lastEntry = 0;
  61.  
  62. for (let punch of punches) {
  63. const punchParts = punch.split(":");
  64. const time = parseInt(punchParts[1].trim()) * 60 + parseInt(punchParts[2]);
  65.  
  66. if (all_enter_options.includes(punchParts[0])) {
  67. if (lastEntry != 0) {
  68. totalElement.innerHTML = "Error: Two consecutive entries";
  69. return
  70. } else {
  71. lastEntry = time;
  72. }
  73. } else {
  74. if (lastEntry > time) { // Previous entry was the day before
  75. totalTime += 24 * 60 - lastEntry + time;
  76. } else {
  77. // If there was no last entry, assume it was the day before, and so calculate since midnight, by subtracting 0 in lastEntry
  78. totalTime += (time - lastEntry);
  79. }
  80. lastEntry = 0;
  81. }
  82. }
  83.  
  84. // If last entry was not exited, calculate until now
  85. if (lastEntry != 0) {
  86. const current = new Date();
  87. const exitTime = current.getHours() * 60 + current.getMinutes();
  88. totalTime += (exitTime - lastEntry);
  89. }
  90.  
  91. let remainingTime = totalTime - theoretical_minutes;
  92.  
  93. const remaining_hours_str = Math.floor(Math.abs(remainingTime) / 60).toString().padStart(2, "0");
  94. const remaining_minutes_str = (Math.abs(remainingTime) % 60).toString().padStart(2, "0");
  95.  
  96. if (remainingTime < 0) {
  97. remainingTimeElement.style.color = "red";
  98. remainingTimeElement.innerHTML = `${remaining_message}: -${remaining_hours_str}:${remaining_minutes_str} (${remainingTime} min.)`
  99. } else {
  100. remainingTimeElement.style.color = "green";
  101. remainingTimeElement.innerHTML = `${remaining_message}: ${remaining_hours_str}:${remaining_minutes_str} 🍺 (${remainingTime} min.)`
  102. }
  103.  
  104. const hours_str = Math.floor(totalTime / 60).toString().padStart(2, "0");
  105. const minutes_str = (totalTime % 60).toString().padStart(2, "0");
  106.  
  107. totalElement.innerHTML = `${total_message}: ${hours_str}:${minutes_str} - `;
  108. }
  109.  
  110. let all_enter_options = [];
  111. let total_message = "Total";
  112. let remaining_message = "Saldo";
  113. let last_language_code = "en";
  114. let theoretical_minutes = 8.5 * 60;
  115.  
  116. async function update_language_data() {
  117. const language_code = window.eval('i18nextko.i18n.lng();')
  118. last_language_code = language_code;
  119. const punch_lang_response = await fetch(`https://vtportal.visualtime.net/2/js/localization/vtportal.i18n.${language_code}.json`);
  120. const punch_lang_data = await punch_lang_response.json();
  121.  
  122. const generic_lang_response = await fetch(`https://vtportal.visualtime.net/2/js/localization/dx.all.${language_code}.json`);
  123. const generic_lang_data = await generic_lang_response.json();
  124.  
  125. all_enter_options = [
  126. punch_lang_data.roPunches_TA_in,
  127. punch_lang_data.roPunches_TA_in_cause,
  128. punch_lang_data.roPunches_TA_in_causeHome,
  129. punch_lang_data.roEntry
  130. ];
  131.  
  132. total_message = generic_lang_data[language_code]["dxPivotGrid-total"];
  133. parts = total_message.split(' ');
  134. total_message = parts[parts.length - 1];
  135. total_message = total_message.charAt(0).toUpperCase() + total_message.slice(1);
  136.  
  137. remaining_message = punch_lang_data.roAccrualLbl;
  138. }
  139.  
  140. const get_current_day_info_promise = () => {
  141. return window.eval('new Promise((resolve, reject) => {new WebServiceRobotics(function (t) {resolve(t);}).getEmployeeDayInfo(undefined, - 1);})')
  142. };
  143.  
  144. async function get_theoretical_hours() {
  145. const day_info = await get_current_day_info_promise();
  146. // The request name says "Hours" but is in fact minutes :(
  147. theoretical_minutes = day_info.DayInfo.DayData[0].MainShift.PlannedHours;
  148. }
  149.  
  150. function prepare() {
  151. Promise.all([
  152. update_language_data(),
  153. get_theoretical_hours(),
  154. ]).then(() => {
  155. update_total_time();
  156. setInterval(update_total_time, 5000);
  157. });
  158. }
  159.  
  160. // Wait for the #punchesList element to be present, at that point, it is ready to calculate
  161. const observer = new MutationObserver(function(mutations_list) {
  162. mutations_list.forEach(function(mutation) {
  163. mutation.addedNodes.forEach(function(added_node) {
  164. if(added_node.id == 'punchesList') {
  165. observer.disconnect();
  166. setTimeout(prepare, 100);
  167. }
  168. });
  169. });
  170. });
  171. observer.observe(document.documentElement, { subtree: true, childList: true });
  172.  
  173. var link = document.querySelector("link[rel~='icon']");
  174. if (!link) {
  175. link = document.createElement('link');
  176. link.rel = 'icon';
  177. document.getElementsByTagName('head')[0].appendChild(link);
  178. }
  179. link.href = 'data:image/png;base64,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';